Stojan Anastasov
Posted on September 8, 2020
Google introduced Jetpack, a family of opinionated libraries to make Android development easier a few years ago. One of the core classes in Jetpack is LiveData
- an observable, lifecycle aware data holder. The typical use case is having a ViewModel
that exposes LiveData
as a property, and observing it from your lifecycle owner, a Fragment
or an Activity
.
A typical usage would look like this:
data class MyState(val value: String)
class MyViewModel : ViewModel {
private val _state = MutableLiveData<MyState>()
val state: LiveData<MyState>
get() = _state
}
class MyFragment : Fragment {
val viewModel by viewModels<MyViewModel>()
override fun onViewCreated() {
viewModel.state.observe(this, Observer(::handleState))
}
private fun handleState(state: MySate): Unit = TODO()
}
There are multiple benefits of using LiveData
:
- Your observer is notified when the data changes
- The observer is only notified of changes when it’s active
- Observers are notified when they become active again, like entering into foreground etc
Check the LiveData docs for all benefits.
LiveData and Events
In situations like showing a Snackbar
/dialog or navigating to a different Activity/Fragment
the ViewModel
also needs to notify the LifecycleOwner
. A plain old LiveData
doesn’t work well here because it caches the last item. As a workaround, in the official Android architecture samples there is a SingleLiveEvent
implementation of LiveData
.
Data Layer
But what about the rest of the app? You can use LiveData
in your data layer, in fact Room, the persistence library from Jetpack, support LiveData
as the return type natively. However while using LiveData
across all the layer in the app is possible, it is less than ideal. The operations are always executed on the Main Thread and it comes with limited number of transformation functions compared to RxJava
or Flow
.
To fix this problem LiveData
comes with adapters for both RxJava
and Flow
from KotlinX Coroutines. This means developers can use RxJava
or Flow
in their data layer and convert to LiveData
in the ViewModel
benefiting from the lifecycle awareness.
State and RxJava
I was working on a sample app last week using the Marvel API, you can check the full code on Github. It uses a fully reactive architecture powered by RxJava. My initial approach was to use LiveData
for the ViewModel
-> Fragment
communication. However I also wanted to benefit from the RxJava
excellent testing support, so I decided to try and do what LiveData
does using RxJava
.
For observing state, RxJava offer BehaviorSubject
, a Subject
that caches the last value it observer and emits it to each subscribed Observer
. That takes care of the caching of the last value and observing changes. For observing on the Main Thread there is RxAndroid
. What about the lifecycle part. To take care of that I created a utility class, built atop LifecycleOwner
to help me dispose of the subscription at the right lifecycle callback.
class LifecycleDisposable(
private val disposable: Disposable
) : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
disposable.dispose()
super.onDestroy(owner)
}
}
the code using this:
class RxViewModel : ViewModel() {
private val _state = BehaviorSubject.create<MyState>()
val state: Observable<MyState>
get() = _state
}
class RxFragment : Fragment() {
val viewModel by viewModels<MyViewModel>()
fun onViewCreated() {
viewModel.state
.subscribe(::handleState)
.autoDispose() // extension function, check the Github link
}
private fun handleState(state: MySate) = TODO()
}
Unlike LiveData
, this starts observing in onViewCreated
and unsubscribes in onDestroyView
. The code would be similar to subscribe in onStart
and stop observing in onStop
, however in my experience (and I could be wrong) updating view state while the view is not active never caused me problems.
Events and RxJava
Events are a bit different than state. The main requirement is to be delivered exactly once (they are consumable). Another important requirement is to be delivered ONLY when the view is active (between onStart
and onStop
). My first attempt was using UnicastSubject
:
A Subject that queues up events until a single Observer subscribes to it, replays those events to it until the Observer catches up and then switches to relaying events live to this single Observer until this UnicastSubject terminates or the Observer unsubscribes.
however that didn’t work out well. The queuing of events when there was no observers was working great. Upon subscription all events would be delivered. When there was a subscriber events were getting delivered. The problem was when the subscriber unsubscribes and a new subscriber tries to subscribe (onStart
and onStop
can be triggered multiple times). That led me to UnicastWorkSubject from RxJavaExtensions
:
A Subject variant that buffers items and allows one Observer to consume it at a time, but unlike UnicastSubject, once the previous Observer disposes, a new Observer can subscribe and resume consuming the items.
which was exactly what I needed. For the lifecycle part, again I turned to LifecycleObserver
and created:
class EffectsObserver<E>(
private val effects: Observable<E>,
private val executeEffect: (E) -> Unit
) : DefaultLifecycleObserver {
private var disposable: Disposable? = null
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
disposable = effects.flatMap {
Observable.fromCallable { executeEffect(it) }
}.subscribe()
}
override fun onStop(owner: LifecycleOwner) {
disposable!!.dispose()
super.onStop(owner)
}
}
the usage looks like:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(EffectsObserver(viewModel.effects) { effect ->
when (effect) {
is NavigateToDetails -> TODO("Navigate")
}
})
}
this will make sure events such as navigation and showing a Snackbar
are queued when the app is in the background and happen only when the app is in foreground. It helps avoid the dreaded java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState.
Note: when using this with a Fragment
it is important to register it in onCreate
(called exactly once) and not in onViewCreated
(may be called multiple times with fragments on the backstack).
Conclusion
You can emulate the way LiveData
works for ViewModel
-> LifecycleOwner
communication with RxJava
. However there are a few edge cases that you need to consider to do it right, like delivering events only when the LifecycleOwner
is active.
If you have a different idea to achieve this leave a comment below. Happy coding!
Posted on September 8, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.