Android: Reactive View Part III - Rx Subjects

Tan Jun Rong avatar

Tan Jun Rong

This post is the 3rd part of the series of blog posts I'm writing about making Android view layer Reactive.

  1. Android Reactive View Part I
  2. Android Reactive View Part II
  3. Android: Reactive View Part III - Rx Subjects ← This Post Is Here!
Photo by Jamie Street on Unsplash
Photo by Jamie Street on Unsplash

In Part I and Part II, I wrote about how buttons are made reactive. By using RxBinding's extension function, it's pretty easy:

val button: Button
val observable: Observable<Unit> = button.clicks()

The implementation of the .clicks() methods can be found here (ViewClickObservable.kt). If you look into the implementation, this line here shows that the setOnClickListener() of the View to re-emit the click event:

override fun onClick(v: View) {
    if (!isDisposed) {
        observer.onNext(Unit)
    }
}

Without using the RxBinding, there are a couple of ways to make something Reactive, through the various RxJava Observable creation methods.

When none of this methods fit, a Subject can be used as a bridge to make something Reactive.

In this post, let's look at these 2 cases:

  • how RecyclerView can be made reactive
  • how startActivityForResult() and onActivityResult() can be made reactive

Subjects

First let's take a look at Subject, the definition says that it is:

a sort of bridge or proxy that is available in some implementations of ReactiveX that acts both as an observer and as an Observable. Because it is an observer, it can subscribe to one or more Observables, and because it is an Observable, it can pass through the items it observes by reemitting them, and it can also emit new items.

I often find definition hard to understand. Here's my attempt to explain Subject. It is something that can receive events (blue) and also emitting events (red).

My proud doodle of Subject
My proud doodle of Subject

What it means is that if you have a few Observables, for example:

  • button1.clicks()
  • button2.clicks()
  • button3.clicks()

A subject can be used to receive from them.

val button1Obs = button1.clicks().map { 1 }
val button2Obs = button2.clicks().map { 2 }
val button3Obs = button3.clicks().map { 3 }
val subject = PublishSubject.create<String>()

Observable.merge(button1Obs, button1Obs, button1Obs)
    .subscribe(subject)

After receiving signals from the buttons, subject can re-emit them. Since subject is emitting, it can be subscribed .

Note: It depends on which type of Subject it is, the timing of subscription determines what the Observer will receive

Continuing from the example above, the subject can be subscribed() to show a log message like below. 👇

subject.subscribe { buttonNumber ->
    Log.d("debug", "button ${buttonNumber} is clicked!") 
}

// output:
//   button 1 is clicked!
//   button 2 is clicked!
//   button 3 is clicked!
Alternative

Another way to re-emit with Subject is by using the onNext() method. Here's the syntax, check the difference in the syntax:

val button1Obs = button1.clicks().map { 1 }
val button2Obs = button2.clicks().map { 2 }
val button3Obs = button3.clicks().map { 3 }
val subject = PublishSubject.create<String>()

Observable.merge(button1Obs, button1Obs, button1Obs)
-    .subscribe(subject)
+    .subscribe { subject.onNext(it) }
Types of Subject

There are a few types of Subject:

  • PublishSubject
  • BehaviourSubject
  • ReplaySubject

is out of the scope of this post, feel free to head over to these post series by David Karnok for a deeper dive.


Using Subject for RecyclerView

For RecyclerView, the use of Subject can be used in a similar way to receives click events and re-emit them.

Subject receives from ViewHolder and re-emit them
Subject receives from ViewHolder and re-emit them

Let's say we have a MainActivity, containing a RecyclerView and an Adapter:

Simple RecyclerView with row1, 2 ,3
Simple RecyclerView with row1, 2 ,3

The code for MainActivity.kt and MainAdapter.kt:

val adapter = MainAdapter()

val list = listOf("row1", "row2", "row3")
fun onCreate() {
    setContentView(R.layout.activity_reactive_views)
    // setup RecyclerView
    recyclerView.adapter = adapter 
    adapter.submitList(list) 
}
class MainAdapter : ListAdapter<String, RecyclerView.ViewHolder>(...) {
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val textToDisplay = getItem(position)
        (holder as MainViewHolder).bind(textToDisplay)
    }

    class MainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(textToDisplay: String) {
            itemView.apply {
                rowTextView.text = textToDisplay
            }
        }
    }
}

Making it Reactive

Now let's make the Adapter reactive 👇
By passing an onClickCallback into the MainViewHolder, subject.onNext(it) can be called inside.

class MainAdapter : ListAdapter<String, RecyclerView.ViewHolder>(...) {
+    val subject = PublishSubject.create<String>()
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val textToDisplay = getItem(position)
+        (holder as MainViewHolder).bind(textToDisplay) {
+            subject.onNext(it)
+        }
    }

    class MainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        fun bind(textToDisplay: String, onClickCallback: () -> Unit) {
            itemView.apply {
                rowTextView.text = textToDisplay
+               setOnClickListener { onClickCallback.invoke(textToDisplay) }
            }
        }
    }
}

Now that MainAdapter is reactive, we can subscribe to it from the MainActivity:

val adapter = MainAdapter()

val list = listOf("row1", "row2", "row3")
fun onCreate() {
    setContentView(R.layout.activity_reactive_views)
    // setup RecyclerView
    recyclerView.adapter = adapter 
    adapter.submitList(list)
+   adapter.subject
+       .subscribe { rowText ->
+           Log.d("debug", "${rowText} is clicked!") 
+       }
}

That's all! The click event is now propagated from ViewHolder to Adapter, back to Activity upwards, by using Subject as a bridge.

Let's see how to make a call that requires startActivityForResult() and onActivityResult() to become reactive.

Using Subject for onActivityResult()

Let's say we have a MainActivity.kt trying to call another activity for picking an image, ImagePickingActivity.

On button click, we use startActivityForResult().

val imagePickedSubject:PublishSubject<Uri> = PublishSubject.create()

button.setOnClickListener {
    val intent = Intent(activity, ImagePickingActivity::class.java)
    startActivityForResult(intent, ImagePickingActivity.REQUEST_IMAGE_CAPTURE)
}

The result would then come back from onActivityResult():

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == RESULT_OK) {
            when (requestCode) {
                ImagePickingActivity.REQUEST_IMAGE_CAPTURE -> {
                   data?.getStringExtra(...)?.let { imagePath ->
+                        imagePickedSubject.onNext(imagePath)
                    }
                }
            }
        }
    }

Since this is now reactive, it can be passed into the ViewModel, and being subscribed() by an observer to perform any actions.

class MainViewModel(val imagePickedObservable: Observable) {
    fun onCreate() {
        imagePickedObservable
            .doOnNext { /* perform action */ }
            .flatMap { /* perform action */ }
            .subscribe { /* perform action */ }
    }
}

That's it, it's pretty straight forward.

Things to becareful when Subject

1.

Since Subject is both receives and emits events. It should be handled with care. To make it clearer, let's consider this diagram:

In the diagram, Activity is supposed to be receiving events from the Subject, however, there's nothing preventing Activity from using this Subject from Adapter to re-emit events.

What we can do is to use the .hide() method from the subject, this will make a Subject to lose it's ability to re-emit events:

val observable = subject.hide()

However, the documentation says that by calling .hide(), some optimizations will be prevented, so it's probably good to not call this method, unless you're writing a library 💭 .

I recently learn that casting a Subject to Observable works too:
https://blog.kaush.co/2019/03/30/rx-tip-hide-your-subjects/

2.

Since Subject is a kind of bridge between the non-Rx and the Rx world, it's best not to use it when it's not necessary. Many times, we can get away with using other Observable creation methods: RxJava Observable creation methods.

Closing

Thanks for reading! Using RxJava is a very different paradigm, at the beginning it looks scary to me when I see a big chunk of Rx code, but in fact all it does it connecting things. It's mostly about some input, then some manipulation, then producing some outputs. I find the code to become easier to maintain in a long run.

I hope you find this post useful. 📚📚
See you next time 👋

Tan Jun Rong avatar
Written By

Tan Jun Rong

Android Programmer who likes writing blogs, reading, coffee, snowboarding.
Published in Android
Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.