I am trying to chain few observables together and do some action depending what observable has been executed. But I faced with strange behavior.
class MainActivity : AppCompatActivity() {
val TAG: String = MainActivity::class.java.name
private lateinit var clicker: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
clicker = findViewById(R.id.clicker) as TextView
clicker.setOnClickListener {
val i = AtomicInteger()
getFirstObservable()
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
showMessage(i, it)
}
.flatMap { getSecondObservable() }
.doOnNext {
showMessage(i, it)
}
.flatMap { getThirdObservable() }
.doOnNext {
showMessage(i, it)
}
.subscribe()
}
}
fun getFirstObservable(): Observable<String> {
return Observable.fromCallable {
Thread.sleep(2000)
"Hello"
}
}
fun getSecondObservable(): Observable<Int> {
return Observable.fromCallable {
Thread.sleep(2000)
3
}
}
fun getThirdObservable(): Observable<String> {
return Observable.fromCallable {
Thread.sleep(2000)
"World!"
}
}
fun showMessage(i: AtomicInteger, obj: Any) {
val msg = "Message #${i.incrementAndGet()}: from ${Thread.currentThread().name}: $obj"
Log.e(TAG, msg)
clicker.text = msg
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}
In this example logs would be shown every 2 seconds but all changes with views would be done when the last observable will be finished.
12-04 01:11:30.465 19207-19207/com.googlevsky.rxtest E/com.googlevsky.rxtest.MainActivity: Message #1: from main: Hello
12-04 01:11:32.473 19207-19207/com.googlevsky.rxtest E/com.googlevsky.rxtest.MainActivity: Message #2: from main: 3
12-04 01:11:34.479 19207-19207/com.googlevsky.rxtest E/com.googlevsky.rxtest.MainActivity: Message #3: from main: World!
I think it is behavior of AndroidScheduler.mainThread(), because when I remove this line and wrap changes with views like this
Handler(Looper.getMainLooper()).post {
clicker.text = msg
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
behavior becomes right. So can someone explain this behavior and suggest a correct way to solve this problem?
Most of your code is being executed on the the main thread, including the sleeps. When an observable is created, it is subscribed on and observed on the current thread unless otherwise specified. When you create your second and third observables, they are on the main thread. Furthermore, since there is no backgrounding of the observable work, it is executed immediately on the current thread when you subscribe. Therefore, all the work and observations happen on the main thread without yielding back to the android OS. The UI is blocked waiting for time on the main thread. If you increase those sleep times, you can force an ANR. To fix it, you can specify observeOn and subscribeOn for each of your observables to push the work to a computation thread for each of them.
getFirstObservable().subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
showMessage(i, it)
}
.flatMap {
getSecondObservable().subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
}
.doOnNext {
showMessage(i, it)
}
.flatMap {
getThirdObservable().subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
}
.doOnNext {
showMessage(i, it)
}
.doOnNext {
showMessage(i, it)
}
.subscribe()
Related
Problem
The issue with reactive programming patterns for one-time events is that they may be re-emitted to the subscriber after the initial one-time event has occurred.
For LiveData the SingleLiveEvent provides a solution using an EventObserver which may also be applied to Kotlin Flow.
Question
Can an AsyncSubject observable be created to handle the case of the SingleLiveEvent in RxJava? The main issue seems to be if there a way for an AsyncSubject to be manually "re-opened" to re-emit data after onComplete is called?
Potential solution
AsyncSubject seems like a potential solution for RxJava, without creating an EventObserver, as the documentation states that it will only publish it when the sequence is completed.
Implementation - Loading status sample
A loading boolean is emitted from the ViewModel method initFeed and view effect state to the view, a fragment in this case. The loading boolean works as expected on the initialization of the fragment and ViewModel sending true via onNext, and completing with onComplete on either a successful or erroneous attempt.
However, the attempt to re-emit a value fails when for example a swipe to refresh initiates the same initFeed method. It seems that onNext cannot be used after onComplete is called for the same object.
SomeViewEffect.kt
data class _FeedViewEffect(
val _isLoading: AsyncSubject<Boolean> = AsyncSubject.create(),
)
data class FeedViewEffect(private val _viewEffect: _FeedViewEffect) {
val isLoading: AsyncSubject<Boolean> = _viewEffect._isLoading
}
SomeViewModel.kt
private fun initFeed(toRetry: Boolean) {
val disposable = feedRepository.initFeed(pagedListBoundaryCallback(toRetry))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { results ->
when (results.status) {
LOADING -> {
Log.v(LOG_TAG, "initFeed ${LOADING.name}")
_viewEffect._isLoading.onNext(true)
}
SUCCESS -> {
Log.v(LOG_TAG, "initFeed ${SUCCESS.name}")
_viewEffect._isLoading.onNext(false)
_viewEffect._isLoading.onComplete()
_viewState._feed.onNext(results.data)
}
ERROR -> {
Log.v(LOG_TAG, "initFeed ${ERROR.name}")
_viewEffect._isLoading.onNext(false)
_viewEffect._isLoading.onComplete()
_viewEffect._isError.onNext(true)
}
}
}
disposables.add(disposable)
}
SomeFragment.kt
private fun initViewEffects() {
val isLoadingDisposable = viewModel.viewEffect.isLoading
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError { Log.v(LOG_TAG, "Error loading isLoading") }
.subscribe { isLoading ->
if (isLoading) progressBar.visibility = VISIBLE
else {
progressBar.visibility = GONE
swipeToRefresh.isRefreshing = false
}
}
compositeDisposable.addAll(isLoadingDisposable, isErrorDisposable)
}
It is not very clear why you need AsyncSubject which emits only last event. Did you try to use Behavior or Publish Processor for this situation?
Use an Event Wrapper
An AsyncSubject does not seem to be a suitable solution to handle one-time occurrence emissions from an Observable to a Subscriber. After onComplete is called an AsyncSubject can not "re-open" to emit future one-time events.
Using an event wrapper such as an Event, as outlined in LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) is the best approach.
FeedViewEffect.kt
data class _FeedViewEffect(
val _isLoading: BehaviorSubject<Event<Boolean>> = BehaviorSubject.create()
)
data class FeedViewEffect(private val _viewEffect: _FeedViewEffect) {
val isLoading: BehaviorSubject<Event<Boolean>> = _viewEffect._isLoading
}
FeedViewModel.kt
private fun initFeed(toRetry: Boolean) {
val disposable = feedRepository.initFeed(pagedListBoundaryCallback(toRetry))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { results ->
when (results.status) {
LOADING -> _viewEffect._isLoading.onNext(Event(true))
SUCCESS -> _viewEffect._isLoading.onNext(Event(false))
ERROR -> _viewEffect._isLoading.onNext(Event(false))
}
}
disposables.add(disposable)
}
FeedFragment.kt
#ExperimentalCoroutinesApi
private fun initViewEffects() {
val isLoadingDisposable = viewModel.viewEffect.isLoading
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError { Log.v(LOG_TAG, "Error loading isLoading") }
.subscribe { isLoading ->
if (isLoading.getContentIfNotHandled() == true) {
progressBar.visibility = VISIBLE
} else {
progressBar.visibility = GONE
swipeToRefresh.isRefreshing = false
}
}
compositeDisposable.addAll(isLoadingDisposable)
}
There are cases when I need to chain RxJava calls.
The simplest one:
ViewModel:
fun onResetPassword(email: String) {
...
val subscription = mTokenRepository.resetPassword(email)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(
//UI update calls
)
...
}
My Repository:
fun resetPassword(email: String): Single<ResetPassword> {
return Single.create { emitter ->
val subscription = mSomeApiInterface.resetPassword(email)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe({
emitter.onSuccess(...)
}, { throwable ->
emitter.onError(throwable)
})
...
}
}
My Question
Do I need to Add:
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
for both calls to avoid any app freeze? or the second one for API call is enough?
No, you don't need to add
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
for the repo and the viewmodel.
.observeOn usually should be called right before handling the ui rendering. So usually, you'll need it in the ViewModel right before updating the ui or emitting the LiveData values.
Also, you properly don't need to subscribe to mSomeApiInterface in your repo, I think it would be better off to just return in as it's from your method up the chain, somthing like this:
fun resetPassword(email: String): Single<ResetPassword> {
return mSomeApiInterface.resetPassword(email);
}
and if you have any mapping needed you can chain it normally
fun resetPassword(email: String): Single<ResetPassword> {
return mSomeApiInterface.resetPassword(email)
.map{it -> }
}
This way you can write your ViewModel code as follow
fun onResetPassword(email: String) {
...
// note the switcing between subscribeOn and observeOn
// the switching is in short: subscribeOn affects the upstream,
// while observeOn affects the downstream.
// So we want to do the work on IO thread, then deliver results
// back to the mainThread.
val subscription = mTokenRepository.resetPassword(email)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
//UI update calls
)
...
}
This will run the API request on the io thread, will returning the result on the mainThread, which is probably what you want. :)
This artical has some good examples and explanations for subscribeOn and observeOn, I strongly recommend checking it.
Observable<RequestFriendModel> folderAllCall = service.getUserRequestslist(urls.toString());
folderAllCall.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(result -> result.getRequested())
.subscribe(this::handleResults, this::handleError);
private void handleResults(List<Requested> folderList) {
if (folderList != null && folderList.size() != 0) {
usersList.addAll(folderList);
}
adapter.notifyDataSetChanged();
}
}
private void handleError(Throwable t) {
Toast.makeText(getContext(),t.getMessage(),Toast.LENGTH_LONG).show();
}
in interface:
#Headers({ "Content-Type: application/json;charset=UTF-8"})
#GET
Observable<RequestFriendModel> getUserRequestslist(#Url String url);
POJO model :
public class RequestFriendModel {
#SerializedName("requested")
#Expose
private List<Requested> requested = null;
public List<Requested> getRequested() {
return requested;
}
public void setRequested(List<Requested> requested) {
this.requested = requested;
}
}
I have a list of completables that by default I run them one after one with concat/andThen operators.
Sometimes I want some part of the completables to run in parallel and after everything complete continue to the next completable in the list.
I tried to achieve that with this code:
var completable =
getAsyncCompletables()?.let {
it
} ?: run {
completables.removeAt(0).getCompletable()
}
while (completables.isNotEmpty()) {
val nextCompletable = getAsyncCompletables()?.let {
it
} ?: run {
completables.removeAt(0).getCompletable()
}
completable = nextCompletable.startWith(completable)
}
completable
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe()
I use this code to detect the async completables:
private fun getAsyncCompletables(): Completable? {
if (completables.size < 2 || !completables[1].async) {
return null
}
var completable = completables.removeAt(0).getCompletable()
while (completables.isNotEmpty() && completables[0].async) {
completable = completable.mergeWith(completables.removeAt(0).getCompletable())
}
return completable
}
All works fine, except one thing, the last completable not triggered althought I used "startWith".
I also tried "concatWith" and "andThen",but same result.
It is a bit difficult to answer without seeing more of your code, specifically what async does and what the data structure is for completables. However, the answer you are looking for is most likely similar regardless of these values. You will probably want to use Completable.merge(...) or Completable.mergeArray(...).
As per the documentation:
/**
* Returns a Completable instance that subscribes to all sources at once and
* completes only when all source Completables complete or one of them emits an error.
* ...
*/
In order to achieve the parallel execution, you will need to call subscribeOn each of your Completables in the list/array/set with a new thread. This can be done with Schedulers.newThread() or from a shared pool like Schedulers.io().
I ran a test just to be sure. Here is the code.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
val completeOne = Completable.fromAction {
Timber.d("Completable #1 running on ${Thread.currentThread().name}")
}
val completeTwo = Completable.fromAction {
Timber.d("Completable #2 running on ${Thread.currentThread().name}")
}
val completeThree = Completable.fromAction {
Timber.d("Completable #3 running on ${Thread.currentThread().name}")
}
val completables = listOf(completeOne, completeTwo, completeThree).map { CompletableWrapper(it) }
val asyncCompletables = completables
.asSequence()
.filter { it.async }
.map { it.getCompletable().subscribeOn(Schedulers.io()) }
.toList()
Completable.merge(asyncCompletables)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Timber.i("Completed all completables")
}, Timber::e)
}
class CompletableWrapper(
private val completable: Completable,
val async: Boolean = true
) {
fun getCompletable() = completable
}
And here is the output.
D/MainActivity$onCreate$completeThree: Completable #3 running on RxCachedThreadScheduler-3
D/MainActivity$onCreate$completeTwo: Completable #2 running on RxCachedThreadScheduler-2
D/MainActivity$onCreate$completeOne: Completable #1 running on RxCachedThreadScheduler-1
I/MainActivity$onCreate: Completed all completables
As you can see, it runs each completable on a new thread from the pool and only calls completed all after each completable has finished.
See here for the documentation on Completable.merge/mergeArray.
I am not getting how to use the result of first observable in second observable.
-> My First Observable
var uploadImgObservable = Observable.create<File> {....}
-> My Second Observable
var thumbnailObservable = Observable.create<Task<UploadTask.TaskSnapshot>> {...}
Now i wanna use the result of uploadImgObservable in thumbnailObservable.
I also tried to use flatmap as suggested by stackoverflow but i didnt get it ..
This is how i used flatmap in my observable..
Observable.create<Task<UploadTask.TaskSnapshot>> { e ->
firebaseStorageReference.child("profile_images").child(current_user_uid+"_thumbnail"+ ".jpg").putFile(imageFile)
.addOnCompleteListener { task: Task<UploadTask.TaskSnapshot> ->
e.onNext(task)
e.onComplete()
}
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.unsubscribeOn(Schedulers.io())
.flatMap(object : Function<Task<UploadTask.TaskSnapshot>,Observable<File>>{
override fun apply(t: Task<UploadTask.TaskSnapshot>): Observable<File> {
var compressedImageBitmap = compress?.setMaxWidth(640)
?.setMaxHeight(480)
?.setQuality(70)
?.setCompressFormat(Bitmap.CompressFormat.WEBP)
?.setDestinationDirectoryPath(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).getAbsolutePath())
?.compressToFile(actualImageFile)
return Observable.just(compressedImageBitmap)
}
})?.subscribe(object : Observer<File>{
override fun onNext(t: File) {
}
override fun onError(e: Throwable) {
}
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
})
As you can see after using flatmap, Observable<Task<UploadTask.TaskSnapshot>> converts to Observable<File> but i dont wanna convert the type of observable after using the result of first observable.
What should i do to use the result of first observable in second observable?
The name of the flatMap has "map" in it which means it will map some value to another. But what you can do is
firstObservable.flatMap(firstObservableResult ->
secondObservable
.flatMap(secondObservableResult -> Observable.just(firstObservableResult)))
.subscribe(firstObservableResult-> {
// process request data
});
Hope you still understand Java code.
I'm trying to do a simple search UI, where the text change triggers a search in the service and that gets mapped to a ViewState. It would seem easy, but the following code doesn't work:
queryText.filter { it.length > 3 }
.switchMap { service.search(it) }
.onErrorReturn { SearchResponse(null, it.message) }
.map { SearchViewState(items = it.items, error = it.error) }
.startWith { SearchViewState(loading = true) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { viewState.onNext(it) }
I've no idea what I did wrong, but through debugging I can see that the stream throws a NetworkOnMainThreadException and then terminates so new events are no longer processed.
What is the correct way to do this?
I assume queryText is the source of textchanges which happen on the main thread. Therefore subscribeOn has no effect on it. You should apply subscribeOn to the actual network call:
queryText.filter { it.length > 3 }
.switchMap {
service.search(it)
.subscribeOn(Schedulers.io())
.onErrorReturn { SearchResponse(null, it.message) }
.map { SearchViewState(items = it.items, error = it.error) }
.startWith ( SearchViewState(loading = true) )
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe { viewState.onNext(it) }
In addition, I think you have to do the error recovery and state changes associated with the particular network call, otherwise a failure will stop the entire sequence.