Reactive Android Apps with Kotlin, ViewModel, LiveData and RxJava
I would like to share with you a way to create reactive Activities and Fragments using the Architecture Components Library, RxJava and Kotlin. I am not going to explain each of them in depth. However, even if you are not familiar with the aforementioned tools, I believe the code provided is simple enough for any Android developer to follow.
I would like to begin with BaseViewModel
class to get some concepts out of the way from the start:
abstract class BaseViewModel<State, Event>(
state: State,
reducer: (State, Event) -> State
) : ViewModel() {
val liveState: LiveData<State>
val events: Observer<Event>
private val disposable: Disposable
init {
liveState = MutableLiveData<State>()
events = PublishSubject.create()
disposable = events.scan(state, reducer)
.subscribe({ liveState.value = it })
}
override fun onCleared() {
super.onCleared()
events.onComplete()
disposable.dispose()
}
}
ViewModel
is a class from the Architecture Components Library for Android. It was created to solve a common issue with the platform: surviving configuration changes (e.g.: rotating the device). They are meant to be used to hold the State of our View (Activity or Fragment). The way we expose the State is by publishing it using LiveData: a sort of Observable that is aware of the life-cycle of our View.
The State is not static. It will change after certain Events occur. We can represent this fact as function: f(old_state, event) = new_state
. This type of functions are commonly referred to as Reducers. What the scan
operator does is to take the latest State and pass it through the Reducer to obtain a new State every time an Event occurs.
Do not worry too much if you do not fully understand the code above. What BaseViewModel
allows us to do is focus on the three main concepts mentioned before: State, Events and Reducer. Next, we are going to define the State of a counter:
data class CounterState(val count: Int)
For such a simple example, we just need to keep track of the current count so we can display on screen. Notice count
is immutable, meaning you cannot assign a different value after State
has been instantiated.
We are now going to define a few Events that can alter the State of our counter:
sealed class CounterEvent {
object Increment : CounterEvent()
data class Add(val quantity: Int) : CounterEvent()
}
We defined only two of the possible events. Increment
does not carry any parameters, so we declare it as an object
. Add
, on the other hand, carries a quantity to be added to the counter, so we declare it as a data
class.
By using Kotlin’s sealed class we can define a finite number of well-known Events that can occur. This is important for when we write the Reducer function for our ViewModel:
class CounterViewModel : BaseViewModel<CounterState, CounterEvent>(
CounterState(count = 0),
{ state, event ->
when (event) {
is CounterEvent.Increment -> state.copy(
count = state.count + 1
)
is CounterEvent.Add -> state.copy(
count = state.count + event.quantity
)
}
}
)
For brevity, I declared the Reducer in the constructor. All we are doing is specifying the initial state, making sure count
is 0 to begin with. When an increment occurs, we copy the previous State and add 1 to count
. When an addition happens, we also copy the previous State and add the quantity determined by the Event into count
. It is important to copy the previous State because sometimes an Event modifies only part of it and we want the rest to remain the same.
As an exercise you can try to add the Decrement
and Substract
events and let the Reducer update the State appropriately. It is now time to put our new ViewModel to use:
class CounterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
val counterView = findViewById<TextView>(R.id.counter)
val viewModel = ViewModelProviders.of(this)
.get(CounterViewModel::class.java)
viewModel.liveState.observe(this, Observer {
it?.let { (count) ->
counterView.text = "Count: $count"
}
}) findViewById<Button>(R.id.increment).setOnClickListener({
viewModel.events.onNext(CounterEvent.Increment)
})
}
}
When we create our activity we subscribe to liveState
and populate the TextView with the new count every time there is an update. As an example, I implemented a Listener for the increment Button so it submits a new CounterEvent.Increment
when clicked.
Conclusion
The approach I described above is not new. The goal is to have a single flow of events and states, forming a cycle that makes it easier to predict how the view is going to behave. By using ViewModel and LiveData we abstract away the complexity of the Activity and Fragment life-cycles, which could potentially introduce issues when handling events and subscriptions.
There are many things which haven’t been covered and I will leave it to the reader to experiment with the approach. However, it is important to mention that Events are not only user actions. In this example, events
is and Observer. Therefore, it can be subscribed to other Observables, regardless of if they are bound to click events or network requests, as long as they return (or are mapped to) an expected Event type.
I hope this work serves a starting point for those of you trying to make your Android applications more reactive.