How to make ViewModel observe Android Lifecycle Events, and how to Unit Test Lifecycle Events
Sometimes we want to perform certain logic when an Android Lifecycle Event occurs. Let’s say your Activity
switches from onCreate()
to onStart()
, and every time the screen appears you want to reload data or do some other work.
Using an MVVM architecture, that might look something like this in your Activity
class:
override fun onStart() {
super.onStart()
myViewModel.onStart() // load data in this method
}override fun onResume() {
super.onResume()
myViewModel.onResume() // or load data in this method
}
However, it might be nice if we could instead have the ViewModel
itself observe the lifecycle
changes. This way we can observe any lifecycle event we want within the ViewModel
itself, and ultimately will be able to write more testable code, with less exposed functions. After all, we don’t want the Activity
to be able to call onStart()
within onCreate()
! To do that, let’s implement LifecycleObserver
in our ViewModel
.
MyViewModel: ViewModel(repository: MyRepository), LifecycleObserver
Great! Do we need to override anything? Nope! Instead, we need to make sure that our ViewModel
now observes the lifecycle of the view it is attached to. So inside our Activity
, we can now add MyViewModel
as an observer. Any changes to the Lifecycle.State
or Lifecycle.Event
will now be transmitted to MyViewModel
.
override fun onCreate(savedInstanceState: Bundle?) {
// ...
lifecycle.addObserver(myViewModel)
}
Next, we need to make sure we respond to the events. In your ViewModel, add a function with the @OnLifecycleEvent
annotation, along with the event type you are listening for.
@OnLifecycleEvent(Lifecycle.Event.ON_START)
private fun onStart() {
repository.reloadData()
}
Great! Your ViewModel will now call repository.reloadData
whenever the lifecycle goes through onStart
.
But how do we test the LifecycleEvents?
The simple way to do this is to simply call viewModel.onStart()
in your JUnit tests, but this is not great since it forces us to expose a public onStart()
method, and it doesn’t ensure we have the correct annotation in the ViewModel to actually observe the real lifecycle.
Instead, let’s have our ViewModel observe some mock event changes in our JUnit tests. First we create a mock LifecycleOwner
using whatever mocking library you want (I’m using mockk). Then pass that into a LifecycleRegistry
, which extends Lifecycle
. This is a class that is used by Android’s Fragment
class (and others) to update the lifecycle state. In our case, we are going to attach the ViewModel
to a stubbed LifecycleRegistry
in order to test our events.
@Test
fun testOnStart() {
val myViewModel = MyViewModel(mockRepository) val mockLifeCycleOwner: LifecycleOwner = mockk()
val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner)
lifecycleRegistry.addObserver(myViewModel) // Trigger the Lifecycle.Event
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) // Test the results how you want
verify { mockRepository.reloadData() }
}
Great! Now the ViewModel
is observing changes. Now since we will be wanting to test lifecycle events quite often, let’s go ahead and make a reusable extension function in our test directory that will work for any LifecycleObserver
.
fun LifecycleObserver.testLifeCycleEvent(lifecycleEvent: Lifecycle.Event) {
val mockLifeCycleOwner: LifecycleOwner = mockk()
val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner)
lifecycleRegistry.addObserver(this)
lifecycleRegistry.handleLifecycleEvent(lifecycleEvent)
}
And now all we have to do to do to test our ViewModel
events is:
@Test
fun onStart_reloadData() {
viewModel.testLifecycleEvent(Lifecycle.Event.ON_START)
verify { mockRepository.reloadData() }
}
That’s it! Clean, reusable code with a ViewModel
that observes lifecycle.