4.
Observables & Subjects in Practice
Written by Alex Sullivan & Marin Todorov
By this point in the book, you understand how observables and different types of subjects work, and you’ve learned how to create and experiment with them in an IntelliJ project.
It could be a bit challenging, however, to see the practical use of observables in everyday development situations, such as binding your UI to a data model, showing a new activity or fragment and getting output out of it.
It’s OK to be a little unsure how to apply these newly acquired skills to the real world. In this book, you’ll work through theoretical chapters such as Chapter 2, “Observables,” and Chapter 3, “Subjects,” as well as practical step-by-step chapters — just like this one!
In the “…in Practice” chapters, you’ll work on a complete app. The starter Android Studio project will include all the non-Rx and other setup code. Your task will be to add the other features using your newly-acquired reactive skills.
That doesn’t mean to say you won’t learn few new things along the way — au contraire!
In this chapter, you’ll use RxJava and your new observable superpowers to create an app that lets users create nice photo collages — the reactive way.
Getting started
Open the starter project for this chapter, Combinestagram, in Android Studio 4.0 or newer. It takes a couple of tries to roll your tongue just right to say the name, doesn’t it? It’s probably not the most marketable name, but it will do.
Add the dependencies for RxJava, RxKotlin and RxAndroid in the app/build.gradle file:
implementation "io.reactivex.rxjava3:rxkotlin:3.0.0"
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
implementation "io.reactivex.rxjava3:rxjava:3.0.2"
Since RxJava 3.0 uses Java 8 features, you’ll also need to let gradle know that you intend to use those features. Add the following in the android
block of the same file:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
Sync the gradle file, build and run the app, and you’ll see the beginnings of the project you’ll bring to life:
In this screen, the user can see their collage as the app builds it. They can add new photos to the collage, clear out the contents of the collage or save it to their phone.
Feel free to take a peek into the utility X.kt file where a list of Bitmap
s is converted into a collage.
You’ll also notice a few other classes in the project. There’s a PhotosBottomDialogFragment to select photos for the collage and a SharedViewModel, which is a ViewModel
that that the MainActivity and PhotosBottomDialogFragment will share.
In this chapter, you are going to focus on putting your new skills to practice. Time to get started!
Using a BehaviorSubject
in a ViewModel
Start by adding a BehaviorSubject
, a CompositeDisposable
, and a MutableLiveData
to the SharedViewModel class:
// 1
private val disposables = CompositeDisposable()
// 2
private val imagesSubject: BehaviorSubject<MutableList<Photo>>
= BehaviorSubject.createDefault(mutableListOf())
// 3
private val selectedPhotos = MutableLiveData<List<Photo>>()
- The
CompositeDisposable
for the subscriptions. - The
imagesSubject
will emitMutableList<Photo>
values. - Finally, you’ll use the
selectedPhotos
variable, that is aLiveData
object, to stream a list of photos to the MainActivity.
Note: It may seem a little strange to use
LiveData
and RxJava in the same project, since they’re both streaming libraries that implement the Observer pattern. However, they actually both have unique strengths and weaknesses that you can utilize to build better apps. You’ll see more details about this in Chapter 22, “Building a Complete RxJava App”.
Take a look at the Photo
data class. It contains a Drawable
resource ID. You’ll use that later on to actually build the collage.
Next up, add the following code to the SharedViewModel class:
init {
imagesSubject.subscribe { photos ->
selectedPhotos.value = photos
}.addTo(disposables)
}
fun getSelectedPhotos(): LiveData<List<Photo>> {
return selectedPhotos
}
You’re subscribing to the imagesSubject
stream and updating the selectedPhotos
value with the items emitted by the subject. Since you’re a responsible RxJava user, you’re adding the Disposable
returned by the subscribe()
method to the CompositeDisposable
you created earlier.
Speaking of being responsible RxJava users, add the following code below the init
block:
override fun onCleared() {
disposables.dispose()
super.onCleared()
}
ViewModel
s onCleared()
method is a great place to dispose of any disposables you may have lying around. Since a ViewModel
is only cleared when the Activity
that created it finishes, you won’t prematurely finish your Observable
s and Subject
s, and you won’t leak any memory.
Adding photos
It’s time to start adding some photos to the collage. Add the following code to the SharedViewModel:
fun addPhoto(photo: Photo) {
imagesSubject.value?.add(photo)
imagesSubject.onNext(imagesSubject.value!!)
}
addPhoto()
takes a Photo
object and adds it to the current list of photos in the collage.
Since you’re using a BehaviorSubject
, you can easily extract the current list of photos from it and add this new photo to that list. You then emit that list again to notify any observers of the newly updated list of photos.
Navigate to MainActivity and replace the println()
call in actionAdd()
with the following:
viewModel.addPhoto(PhotoStore.photos[0])
For now, you’re always using the first photo from the static list of photos that comes shipped with the app. Don’t worry, you’ll update that later on.
It’s time to hook everything up and see a collage! Add the following to the bottom of the onCreate()
method of MainActivity, importing androidx.lifecycle.Observer
when prompted:
// 1
viewModel.getSelectedPhotos().observe(this, Observer { photos ->
photos?.let {
// 2
if (photos.isNotEmpty()) {
val bitmaps = photos.map {
BitmapFactory.decodeResource(resources, it.drawable)
}
// 3
val newBitmap = combineImages(bitmaps)
// 4
collageImage.setImageDrawable(
BitmapDrawable(resources, newBitmap))
}
}
})
The code may seem complicated, but it’s actually very simple:
- You’re observing the
selectedPhotos
live data, which emits lists ofPhoto
objects. - Then, if there are any photos, you’re mapping each
Photo
object to aBitmap
using theBitmapFactory.decodeResource()
method. - Next up, you’re combining that list of bitmaps using the
combineImages()
method. - Finally, you’re setting the
collageImage
image view with the combined bitmap.
Run the app. When you tap the Add button you should see images in the central collage image view. Tab the button again to add more images.
Looking good! Now try to tap the Clear button.
You haven’t hooked up the Clear action yet, so nothing should happen. If something does happen, then that’s magical, and you’ve discovered a new way of building apps without writing any code!
Add the following function to the SharedViewModel class:
fun clearPhotos() {
imagesSubject.value?.clear()
imagesSubject.onNext(imagesSubject.value!!)
}
clearPhotos()
works very similarly to addPhotos()
, except instead of adding a new photo into the existing list you’re clearing out that list and emitting the now empty list.
Now, navigate back to the MainActivity
class and replace the println()
statement in the actionClear()
method with the following:
viewModel.clearPhotos()
Last but not least, add this else
statement to the if
statement in the selected photos observing code in the onCreate()
:
if (photos.isNotEmpty()) {
// ...
} else {
collageImage.setImageResource(android.R.color.transparent)
}
Now if the photos
list has no photo objects in it, you’re clearing out the image in the collageImage
image view.
Run the app again. This time you should be able to clear photos.
Recapping reactive programming
Reactive programming can be hard to follow at times, so here’s a recap of what’s happening in the app so far:
- Whenever the user taps the Add button, the
MainActivity
class is calling theaddPhoto()
method inSharedViewModel
with a single static photo. - The SharedViewModel class then updates a list of photos that is stored in
imagesSubject
, and it callsonNext()
with the updated list of photos. - Since the view model is subscribed to
imagesSubject
, it receives theonNext()
notification and forwards the new list of photos through to theselectedPhotos
live data. - Since the
MainActivity
class is subscribing to theselectedPhotos
live data, it’s notified of the new list of photos. It then creates the combined bitmap of photos and sets it on thecollageImage
image view. If the list of photos is empty, it instead clears that image view.
At this stage of the app, this may seem like overkill. However, as you continue to improve the Combinestagram app, you’ll see that this reactive stream-based approach has many advantages!
Driving a complex UI
As you play with the current app, you’ll notice the UI could be a bit smarter to improve the user experience. For example:
- You could disable the Clear button if there aren’t any photos selected just yet or in the event the user has just cleared the selection.
- Similarly, there’s no need for the Save button to be enabled if there aren’t any photos selected.
- You could also disable the save functionality for an odd number of photos, as that would leave an empty spot in the collage.
- It would be nice to limit the amount of photos in a single collage to six, since more photos simply look a bit weird.
- Finally, it would be nice if the activity title reflected the current selection.
Let’s set out now to add these improvements to Combinestagram.
Open up MainActivity.kt and add an updateUi()
method below onCreate()
:
private fun updateUi(photos: List<Photo>) {
saveButton.isEnabled =
photos.isNotEmpty() && (photos.size % 2 == 0)
clearButton.isEnabled = photos.isNotEmpty()
addButton.isEnabled = photos.size < 6
title = if (photos.isNotEmpty()) {
resources.getQuantityString(R.plurals.photos_format,
photos.size, photos.size)
} else {
getString(R.string.collage)
}
}
In the above code, you update the complete UI according to the ruleset we’ve talked about. All the logic is in a single place and easy to read through. Now add a call to updateUi()
to the bottom of the Observer
lambda observing for selectedPhotos
:
if (photos.isNotEmpty()) {
// ...
} else {
// ...
}
updateUi(photos)
Run the app again, and you will see all the rules kick in as you play with the UI:
By now, you’re probably starting to see the real benefits of Rx when applied to your Android apps. If you look through all the code you’ve written in this chapter, you’ll see there are only a few simple lines that drive the whole UI!
Communicating with other views via subjects
Combinestagram is almost perfect. But users may want to actually pick from multiple photos instead of just one hardcoded one. Maybe.
Instead of serving up one static image, you’ll instead display a bottom dialog fragment wherein the user can select from a list of photos.
First off, delete the addPhoto()
method in SharedViewModel.
Next up, replace the contents of actionAdd()
in MainActivity with the following:
val addPhotoBottomDialogFragment =
PhotosBottomDialogFragment.newInstance()
addPhotoBottomDialogFragment
.show(supportFragmentManager, "PhotosBottomDialogFragment")
The above code simply shows the PhotosBottomDialogFragment dialog when the user taps the Add button. Try it out now, by running the app. You should see the following after tapping the Add button:
This is the stage in which you’d normally use an interface to have the PhotosBottomDialogFragment
communicate that the user selected a photo. However, that’s not very reactive — so, instead, you’ll use an observable.
Navigate to PhotosBottomDialogFragment and create a new PublishSubject<Photo>
variable:
private val selectedPhotosSubject =
PublishSubject.create<Photo>()
val selectedPhotos: Observable<Photo>
get() = selectedPhotosSubject.hide()
You’ll see this pattern employed often. You created a new PublishSubject
, but you don’t want to expose that subject to other classes because you want to make sure that you know what’s being put into it. Instead of directly exposing selectedPhotosSubject
, you create a new public selectedPhotos
property that returns selectedPhotosSubject.hide()
. The hide()
method simply returns an Observable
version of the same subject.
Add the following to the empty photosClicked()
method:
selectedPhotosSubject.onNext(photo)
You’re now forwarding a photo that a user selected through your subject.
All that’s left to do is to subscribe to this new observable.
Navigate over to SharedViewModel and add the following method:
fun subscribeSelectedPhotos(selectedPhotos: Observable<Photo>) {
selectedPhotos
.doOnComplete {
Log.v("SharedViewModel", "Completed selecting photos")
}
.subscribe { photo ->
imagesSubject.value?.add(photo)
imagesSubject.onNext(imagesSubject.value!!)
}
.addTo(disposables)
}
subscribeSelectedPhotos()
takes an Observable<Photo>
and subscribes to that observable, forwarding the photos it receives through to the imagesSubject
.
Now, navigate back to MainActivity and add the following line in the bottom of the actionAdd()
method:
viewModel.subscribeSelectedPhotos(
addPhotoBottomDialogFragment.selectedPhotos)
You’re ready to go!
Run the app and you should be able to add different photos to your collage:
Cleaning up observables: Review
The code seemingly works as expected, but try the following: Add few photos to a collage, go back to the main screen and inspect logcat.
Do you see a message saying “completed selecting photos”? No? You added a Log
statement to that last subscription using the doOnComplete()
operator that should notify you that the provided selectedPhotos
has completed.
Since the selectedPhotos
observable never completes, the memory it’s utilizing will not be freed until the SharedViewModel
is itself cleared.
If the user keeps going back and forth adding new photos and presenting that bottom dialog fragment, that means more and more observables will be created, since one’s created for every instance of PhotosBottomDialogFragment
. Those observables are taking up precious memory!
Open PhotosBottomDialogFragment and add the following method:
override fun onDestroyView() {
selectedPhotosSubject.onComplete()
super.onDestroyView()
}
Now, whenever the view is destroyed, the selectedPhotosSubject
will be completed and its memory will be reclaimed. You can see that this is true if you run the app, select a photo, then dismiss the bottom sheet. The log statement now prints out.
Perfect! You’re now ready for the last part of this chapter: taking a plain old boring function and converting it into a super-awesome and fantastical reactive one.
Creating a custom observable
You may have noticed that there’s one aspect of the app that doesn’t work yet – saving a photo. Time to fix that!
Open SharedViewModel and take a look at the saveBitmapFromImageView()
method. It’s pretty simple — it just takes an ImageView
and saves its bitmap to the external files directory.
There’s only one problem: It’s boring. Actually, the real problem is that, after you save the photo, there’s no way to figure out where it was saved to — and it’s a blocking call! Both problems that can be fixed by making this function an awesome reactive function.
First, change the return type of saveBitmapFromImageView()
to Observable<String>
. Then, wrap the existing function body in an Observable.create
call like so:
fun saveBitmapFromImageView(
imageView: ImageView,
context: Context
): Observable<String> {
return Observable.create { emitter ->
// Body of the method
// ...
}
}
You’re now returning an observable. However, that observable never emits anything and never finishes. Not all that useful.
Add this to the end of the try
block:
emitter.onNext(tmpImg)
emitter.onComplete()
Then, add this to the end of the catch
block:
emitter.onError(e)
You’re emitting the name of the newly created file and then completing. If the file fails to save, you’re instead emitting that error.
Navigate back to MainActivity and update the actionSave()
method to the following:
viewModel.saveBitmapFromImageView(collageImage, this)
.subscribeBy(
onNext = { file ->
Toast.makeText(this, "$file saved",
Toast.LENGTH_SHORT).show()
},
onError = { e ->
Toast.makeText(this,
"Error saving file :${e.localizedMessage}",
Toast.LENGTH_SHORT).show()
}
)
Build and run the App to test the Save functionality.
So you’ve created an observable that either emits one item and completes or emits an error. That sounds familiar…
Review: Single, Maybe, Completable
In Chapter 2, “Observables,” you had the chance to learn about a few specialized RxJava types.
In this chapter, you’ll do a quick review and see how you might use these types in an app, and then use one of the types in the Combinestagram project! Starting with Single
:
Single
As you already know, Single
is an Observable
specialization. It represents a sequence, which can emit just once either a success
event or an error
.
This kind of type is useful in situations such as saving a file, downloading a file, loading data from disk or basically any asynchronous operation that yields a value. You can categorize two distinct use-cases of Single
:
-
For wrapping operations that emit exactly one element upon success, just like
saveBitmapFromImageView()
earlier in this chapter. You can directly create aSingle
instead of anObservable
. In fact, you will update thesaveBitmapFromImageView()
method inSharedViewModel
to create aSingle
shortly. -
To better express your intention to consume a single element from a sequence and ensure if the sequence emits more than one element the subscription will error out. To achieve this, you can subscribe to any observable and use
singleOrError()
operator to convert it to aSingle
.
Maybe
Maybe
is quite similar to Single
with the only difference that the observable may or may not emit a value upon successful completion.
If you keep to the photograph-related examples, imagine this use-case for Maybe
: your app is storing photos in its own custom photo album. You persist the album identifier in SharedPreferences
and use that ID each time to “open” the album and write a photo inside.
You would design an open(albumId): Maybe<String>
method to handle the following situations:
- In case the album with the given ID still exists, just emit a
completed
event. - In case the user has deleted the album in the meanwhile, create a new album and emit a
next
event with the new ID, so you can persist it inSharedPreferences
. - In case something is wrong and you can’t access the Photos library at all, emit an
error
event.
Just like other the specialized types, you can achieve the same functionality by using a “vanilla” Observable
, but Maybe
gives more context both to you as you’re writing your code and to the programmers coming to alter the code later on.
Just as with Single
, you can create a Maybe
directly by using Maybe.create { ... }
. Or, if you have an existing observable, you can use the firstElement()
or lastElement()
methods to get a Maybe
of that element.
Completable
The final type to cover is Completable
. This variation of Observable
allows only for a single completed
or error
event to be emitted before the subscription is disposed of.
You can create a Completable
sequence by using Completable.create { ... }
with code very similar to that which you’d use to create other observables. You can also use the ignoreElements()
method on an Observable
to get a completable version of it that ignores all the elements.
You might notice that Completable
simply doesn’t allow for emitting any values and wonder why would you need a sequence like that. You’d be surprised at the number of use-cases in which you only need to know whether an asynchronous operation succeeded or not.
Here’s an example before going back to Combinestagram
. Let’s say your app auto-saves a document while the user is working on it. You’d like to asynchronously save the document in a background queue, and when completed, show a small notification or an alert box onscreen if the operation fails.
Let’s say you wrapped the saving logic into a function fun saveDocument(): Completable
. This is how easy it is to then express the rest of the logic:
saveDocument()
.andThen(Observable.just(createMessage))
.subscribeBy(onNext = { message ->
message.display()
}, onError = { e ->
showError(e.localizedDescription())
})
The andThen()
operator allows you to chain more completables or observables upon an event and subscribe for the final result. In case, any of them emits an error, your code will fall through to the final onError
lambda.
Having completed ( :] ) a review of the specialized observable types, you can now update saveBitmapFromImageView()
to return a more specialized and appropriate type.
Back to Combinestagram and the problem at hand!
Using Single in the app
Update saveBitmapFromImageView()
to return a Single<Photo>
and replace the relevant calls to Observable
with the sibling calls to Single
:
fun saveBitmapFromImageView(imageView: ImageView, context: Context): Single<String> {
return Single.create { emitter ->
// ...
try {
// ..
emitter.onSuccess(tmpImg)
} catch(e: IOException) {
Log.e("MainActivity", "Problem saving collage", e)
emitter.onError(e)
}
}
}
Fancy!
All that’s left is to change the subscription to this single and save the photo.
Navigate back to MainActivity and update the actionSave()
method to the following:
viewModel.saveBitmapFromImageView(collageImage, this)
.subscribeBy(
onSuccess = { file ->
// ...
},
onError = { e ->
// ...
}
)
You’re utilizing the new reactive version of the saveBitmapFromImageView()
method and subscribing to the Single
that it produces. If the single succeeds, you’re showing a toast indicating that it finished. If it fails, you’re showing a toast with the error message.
Give the app one last triumphant run to save the collage.
To see the saved file, use the Device File Explorer accessed from the View ▸ Tool Windows menu in Android Studio:
Then navigate to the app data directory in the device sdcard folder to see the file. You can double-click the image file to open it.
Before we move on, there’s still one more issue to tackle.
You may have noticed that, when you saved a collage, the Save button freezes in the tapped state and the UI stopped responding. Saving a photo to storage can take a long time, and is best done on a background thread.
To achieve this, you’ll see a sneak peek of one of the cooler parts of Rx: Schedulers.
In the actionSave()
method, add the following code after the call to saveBitmapFromImageView()
and before the call to subscribeBy()
:
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
The subscribeOn()
method instructs the Single
to do its subscription work on the IO
scheduler. The observeOn()
method instructs the single to run the subscribeBy()
code on the Android main thread.
You’ll learn much more about schedulers in Chapter 13, “Intro to Schedulers”. For now, run the app. You should see the save button immediately return to its normal state after being tapped, and the UI should no longer be blocked.
With that, you’ve completed Section I of this book — congratulations!
You are not a young Padawan anymore, but an experienced RxJava Jedi. However, don’t be tempted to take on the dark side just yet. You will get to battle networking, thread switching, and error handling soon enough!
Before that, you must continue your training and learn about one of the most powerful aspects of RxJava. In Section 2, “Operators and Best Practices,” operators will allow you to take your Observable
superpowers to a whole new level!
Key points
- Observables and Subjects exist not just for theory: you use them in real apps!
- RxJava observables can be combined with LiveData to pass events from a view model along to the UI.
- RxJava can be used to create complex-UI interactions with a small amount of declarative code.
- It’s possible and useful to refactor existing non-Rx code into custom observables using
Observable.create
. - The specialized observable types
Single
,Maybe
, andCompletable
should be used when possible to make your intentions clear to both future you and your teammates.
Where to go from here?
Next up, back to some theory as you start Section II.
With your first Rx-enabled app project behind you, it’s time to dig deeper into RxJava and look at how you can manipulate observable streams using operators.