Avoid Repeated Expensive Computations with RxJava
RxJava is a godsend when navigating asynchronous behaviors and patterns in Android apps. Today, we’re gonna take a look at how to ideally use RxJava to avoid repeating expensive operations.
Let’s take an example of a simple application which performs the expensive operation of loading a Bitmap from a raw image file. The UI of the app looks something like this:
When the user taps on the load image button, we’ll load an expensive Bitmap onto an ImageView
above the Button
.
The flow will be something like:
- The user taps the button
- We make a request to load the Bitmap via an
Observable
. - We get the
Bitmap
when the image is loaded and we set it to theImageView
.
We’re going to use the Android Architecture Components’ ViewModel
as our presentation layer and we’ll use an ImageRepository
to actually load the Bitmap from a raw resource and expose it as an Observable
(Note, you can also use LiveData
for this but for the purposes of this blog post, we’ll be using Rx). This is a pattern that is recommended by Google which provides good separation of concerns between presentation, data and view layers.
Our ImageRepository
looks something like this:
The code here is straightforward. We have a method loadImage
which takes a raw resource as a parameter and returns an Observable<Bitmap>
. An easy Rx operator that can be used to defer expensive computation is fromCallable
. We simply use the BitmapFactory
to decode a raw resource to a Bitmap.
This is what our ViewModel
looks like:
We have a custom Factory
class which helps us in passing ImageRepository
as a constructor parameter.
Finally, our Activity:
Here, we simply initialize our ViewModel
and when the user clicks on the Button
, we make a call to load the image.
Since we’re using RxJava, we will want to handle disposing of our subscriptions to avoid memory leaks. We do this using Uber’s AutoDispose library, which handles the subscriptions for us. (Fun Fact: This whole post is based around a sample that I contributed to AutoDispose to demonstrate how AutoDispose would work in real world applications).
When we run the application and click on the “Load Image” button, the image loads! It looks something like:
That’s awesome! We loaded the raw image successfully! BUT WAIT! I accidentally rotated my phone and now this is what my app looks like:
Yup! We lost the state of our application, which is a pretty bad experience for the user. The user would now be completely confused as to what they were doing and where their precious image went. This happened because we didn’t account for screen rotation and other configuration changes.
Problems
When we analyze the flow we took, we can easily see that it is flawed. There are a few problems:
- We aren’t utilizing Rx’s
Observable
stream properly. TheloadImage
function inImageRepository
returns a cold observable. This means that after the Observable emits one item, it completes (callsonComplete
) and basically dies. We aren’t caching any results either so once it emits something, it’s lost. This means that if another screen calls the sameloadImage
, it will again fetch the image from raw resource and do the expensive computations. For a detailed explanation between hot and cold observables, refer this excellent resource. - We only subscribe to the stream when the user clicks on the button. This ties together well into how we’ve structured the application. We’re basically using RxJava stream to do one-time work, instead of taking advantage of its streaming and caching abilities.
Let’s go about fixing these problems. As a reference, here’s the application code for the functionality we’ve done so far.
Caching Results
What we want to achieve is that we cache the Bitmap that was last emitted by our Observable
and reuse that unless we explicitly ask for a fresh reload.
Let’s first take a look at what changes we’ll need to make to the ImageRepository
.
Create a Hot Observable
Simply put, a hot Observable is one which follows it’s own timeline, independent of it’s subscribers. For example, think about mouse movement events. These events will keep emitting, even if no one is listening to them. Once someone starts listening to them, they’ll get the subsequent events.
To transform the loading of an image to a hot Observable, we’ll use Subjects. A Subject is both an Observable as well as a Subscriber. It’s a common paradigm to use a Subject to bridge the gap between non-Rx code and Rx code. There are different kinds of subjects and you can find out more about them here, but for this example we’ll use a BehaviorSubject
. A BehaviorSubject
will emit all items that are emitted after subscribing to it as well as the last emitted item before the subscription.
In our ImageRepository
we will expose a BehaviorSubject
, that will accept an Integer (which represents our raw resource id). We will then map this Integer to load a Bitmap and return this transformed Observable
.
Our updated ImageRepository
looks something like:
Notice how this time, we’ve separated the act of asking for a new image and observing for the result. This makes sense because we might not want to couple the act of loading images and receiving images into the same method.
ViewModel:
Activity:
As you may have noted, we’ve changed our structure this time. We observe for the results when the Activity starts. When the user clicks on the Button
, we simply ask for the results. By decoupling observing the results and the click listener, we’re able to recover the results.
Once you run the application, the image still loads after rotation! However, if you notice, we logged a statement in ImageRepository
when the load from the raw resource happens. Once you rotate the phone, you’ll see that it’s logged again.
2018-08-05 13:41:25.630 12766-12766/com.shaishavgandhi.rxreplayingsharesample D/ImageRepository: Performing expensive operation
2018-08-05 13:41:28.957 12766-12801/com.shaishavgandhi.rxreplayingsharesample D/ImageRepository: Performing expensive operation
This means, we’re getting the image back but we’re actually loading it twice by performing the operation of loading it from raw resource twice.
This happened because when we rotated the phone and resubscribed, the imageId
in the BehaviorSubject
was emitted again (since its the property of BehaviorSubject’s to emit the last emitted value) and we performed the same operation of loading the resource again.
One Last Step
To avoid the expensive operation we will use a library called RxReplayingShare by Jake Wharton to cache the actual results of the Bitmap instead of the imageId
. From the library’s README:
ReplayingShare
is an RxJava 2 transformer which combines replay(1)
, publish()
, and refCount()
operators.
ReplayingShare is an RxJava 2 transformer which combines replay(1), publish(), and refCount() operators.Unlike traditional combinations of these operators, ReplayingShare caches the last emitted value from the upstream observable or flowable only when one or more downstream subscribers are connected. This allows expensive upstream sources to be shut down when no one is listening while also replaying the last value seen by any subscriber to new ones.
By simply appending replayingShare()
to your upstream observable (in our case, our Subject), RxReplayingShare will cache the results and emit them when we subscribe back.
This time, we use a PublishSubject
instead of a BehaviorSubject
. The reason being that we don’t really want to cache the last emitted resourceId, but instead we want to cache the last emitted Bitmap.
Our updated ImageRepository
now looks like:
Now if we run our app, the image loads and if you rotate, we get the cached Bitmap value. Once we check our logs, we’ll see that on rotation, there was no logging! Wohoo!
The same concept can be applied to network calls or any other expensive operations.
The code for the sample is hosted on GitHub. You can find it here. You can also find the intermediate step here.
Finally, you can find the entire AutoDispose sample here.