Android: Reactive View Part III - Rx Subjects
This post is the 3rd part of the series of blog posts I'm writing about making Android view layer Reactive.
- Android Reactive View Part I
- Android Reactive View Part II
- Android: Reactive View Part III - Rx Subjects ← This Post Is Here!
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()
andonActivityResult()
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).
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.
Let's say we have a MainActivity
, containing a RecyclerView
and an Adapter
:
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
Clap to support the author, help others find it, and make your opinion count.