Ways to don't lose a frame with observeOn

38 views
Skip to first unread message

Brais Gabín Moreira

unread,
Aug 22, 2019, 7:11:04 AM8/22/19
to RxJava
Hi, I have a stream that emits in different threads. Normally it emits the first item in the MainThread (I'm using Android) and then in a background thread (usually io). It should be simplified to something like this:

obs = Observable.interval(1, SECOND)
 
.replay(1)
 
.autoConnect(0)

If I do something like this:

obs
 
.observeOn(mainScheduler)
 
.subscribe() // here I'm losing one frame

The first item is emitted in the mainThread but, because I'm using observeOn, I'm losing one frame. I can't remove the observeOn because I need to receive all the items in the main thread. I know that in this easy example I can do something like:

obs2 = Observable.interval(1, SECOND)
 
.observeOn(mainScheduler)
 
.replay(1)
 
.autoConnect(0)

But that's not an option in my real problem.

I found a solution:

obs
 
.concatMapSingle { item ->
   
if (runningInMainThread())
     
Single.just(item)
   
else
     
Single.just(item)
       
.observeOn(mainThread)
 
}
 
.subscribe()

This works fine for my use cases but I'm a bit worried about the side effects that this can have. I know one of them: onError and onComplete are received in an undefined thread but I'm fine with that. I just crash the app when I receive one of those so it's not a big deal for me.

But is there any other side effect that I'm not seeing here? Is there other cleaner or safer way to solve this problem?

And if this solution is fine. Is there any difference between Single.just(item).observeOn(mainThread) and  Single.just(item).subscribeOn(mainThread) in this case?

Thanks!

Justin Breitfeller

unread,
Aug 22, 2019, 8:03:45 AM8/22/19
to RxJava
The autoConnect(0) is going to instantly subscribe to your observable. If you are saying your source is emitting on multiple threads are you sure that before you call subscribe only one item is emitted?

In the terms of your example, are you sure that one second hasn't passed between the autoConnect and the subscribe.

Emissions aren't just dropped by observeOn. You are creating a hot observable that only caches one item. Because of this, it's possible to miss one or more emissions depending on when you subscribe.

Brais Gabín Moreira

unread,
Aug 23, 2019, 5:20:57 AM8/23/19
to RxJava
I don't care about losing old emisions. let's say that between the autoConnect() and the subscription() there's a Thread.sleep(random.nextInt(5000)). This way I don't know if replay can emit a value synchronously or not. I just need the last emitted value as soon as possible. So if it's cached already by reply I want it synchronously and not in the next frame.

In my real world problem I have a hot stream that emits a list of objects that I need to display in the view. The first time it never has the data so it needs to request it to the server. Other times I don't know if the server answered already or not, but if it has I don't want to show the "loading" for one frame. This creates an ugly flickering that i want to avoid.

Justin Breitfeller

unread,
Aug 23, 2019, 7:38:07 AM8/23/19
to RxJava
I understand your issue now. You are saying this is a view problem because you have your view set up to show loading by default?

The most common solution is to have your observable tell you whether it's loading or complete. Here is the common Android implementation.

https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt

So you would add a startWith before the replay(1) with Resource.loading(null). Then change your UI to start with the normal view instead of the loading view.

Another alternative, which I don't really advise, is to create your own scheduler that instantly runs whatever you schedule if you are already on the main thread. RxAndroid is open source so you can look at how they implemented the main thread scheduler and create a new implementation for your needs.

Brais Gabín Moreira

unread,
Aug 23, 2019, 8:35:02 AM8/23/19
to RxJava
Maybe I'm missunderstinding you but I think that this will not fix my problem:
- If there is an observeOn between starWith and the subscribe I'll lose one frame. => flickering
- If there's not... The problem is the same. Because I have the real information in memory but I'm showing first the loading and then the data in the next frame. => flickering too

I know that the "frame" thing seems a bit picky, but in my use case that flickering is really annoying for the user.

With my workaround I get the data without losing any frame. It works great but I'm a bit worried for the side effects that I can be missing...

So my question is: How do I ensure that the subscription runs always in the main thread BUT I don't run the handler.postDelayed(..., 0) when it's not needed because I'm already in the main Thread?

Justin Breitfeller

unread,
Aug 23, 2019, 8:38:26 AM8/23/19
to RxJava
Either of my solutions would handle your problem. As far as the first approach I mentioned, you missed the sentence:

"Then change your UI to start with the normal view instead of the loading view."

So you would only show your loading view if the observable tells you it is loading with that Resource class I mentioned. You will still "miss" a frame, but you won't get the flickering.

If you truly want to avoid observeOn posting when it doesn't need to post, you should make your own scheduler. It's not standard behavior.

Reply all
Reply to author
Forward
0 new messages