Chapters

Hide chapters

Reactive Programming with Kotlin

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 4.0

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Operators & Best Practices

Section 2: 7 chapters
Show chapters Hide chapters

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 Bitmaps 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>>()
  1. The CompositeDisposable for the subscriptions.
  2. The imagesSubject will emit MutableList<Photo> values.
  3. Finally, you’ll use the selectedPhotos variable, that is a LiveData 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()
}

ViewModels 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 Observables and Subjects, 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:

  1. You’re observing the selectedPhotos live data, which emits lists of Photo objects.
  2. Then, if there are any photos, you’re mapping each Photo object to a Bitmap using the BitmapFactory.decodeResource() method.
  3. Next up, you’re combining that list of bitmaps using the combineImages() method.
  4. 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:

  1. Whenever the user taps the Add button, the MainActivity class is calling the addPhoto() method in SharedViewModel with a single static photo.
  2. The SharedViewModel class then updates a list of photos that is stored in imagesSubject, and it calls onNext() with the updated list of photos.
  3. Since the view model is subscribed to imagesSubject, it receives the onNext() notification and forwards the new list of photos through to the selectedPhotos live data.
  4. Since the MainActivity class is subscribing to the selectedPhotos live data, it’s notified of the new list of photos. It then creates the combined bitmap of photos and sets it on the collageImage 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:

  1. For wrapping operations that emit exactly one element upon success, just like saveBitmapFromImageView() earlier in this chapter. You can directly create a Single instead of an Observable. In fact, you will update the saveBitmapFromImageView() method in SharedViewModel to create a Single shortly.

  2. 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 a Single.

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 in SharedPreferences.
  • 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, and Completable 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.