Lifecycle Observing LiveData

In a previous article of mine I wrote about how one can manage a resource that has a unique lifecycle using a combination of the Android Architecture Components Google has offered us. I used a ViewModel which listened to ProcessLifecycle onStart/onStop callbacks to create and tear down an ExoPlayer instance, and had that value propagated up to the UI via a LiveData<Player?> field. This approach worked, but in hindsight it didn’t scale well and gave too much responsibility to the ViewModel. In this article I write about a different way to achieve the same result but with class responsibilities better contained.

To summarize the point of the previous article, I needed an ExoPlayer instance that responds to the ProcessLifecycle. That is, when the app is in the foreground the instance should be set up for playing, but when the app is backgrounded, it should be stopped and released. When the device goes through a configuration change, the ExoPlayer instance should not be torn down and from the user’s perspective there should be a seamless transition between those configurations. The ExoPlayer instance must be torn down, however, when the UI component that contains it is navigated away from.

ViewModels have an implicit lifecycle. They are created on the first onCreate of the UI component they are associated with, and destroyed on that UI component’s last onDestroy. We can make that ViewModel lifecycle explicit by having a ViewModel implement LifecycleOwner, like so:

abstract class LifecycleViewModel : ViewModel(), LifecycleOwner {
private val registry = LifecycleRegistry(this).apply {
currentState = Lifecycle.State.CREATED
}

override fun
getLifecycle(): Lifecycle {
return registry
}
@CallSuper
override fun onCleared() {
registry.currentState = Lifecycle.State.DESTROYED
}
}

The ViewModel lifecycle combined with an application’s process lifecycle provide the necessary callbacks to represent a Player's lifecycle the way I described earlier. That is, when the process lifecycle hits onStart and onStop, the Player instance will be (re)created and torn down, respectively. When the ViewModel lifecycle hits onDestroy, the Player instance will be torn down. The process lifecycle’s onDestroy cannot be used because the scope of the process lifecycle’s creation and destruction will in most cases not align with how you’d want a Player instance to be managed.

The two lifecycles, ViewModel and process, can be encapsulated in a custom Lifecycle which listens to the relevant callbacks described above, and propagates that to any LifecycleObserver listening to that custom Lifecycle. For the sake of brevity, this encapsulation can be found in this LifecycleDelegate.kt file:

The use of this custom Lifecycle class with a Player resource can then be passed to a custom LiveData<Player> concretion that manages them both:

class PlayerLiveData(
private val appContext: Context,
private val lifecycle: Lifecycle
) : LiveData<Player>(), DefaultLifecycleObserver {

override fun
onActive() {
lifecycle.addObserver(this)
}
// Application's onStart
override fun onStart(owner: LifecycleOwner) {
value = /* create Player */
}
// Application's onStop
override fun onStop(owner: LifecycleOwner) {
tearDown()
}
// ViewModel's onCleared
override fun onDestroy(owner: LifecycleOwner) {
lifecycle.removeObserver(this)
tearDown()
}

private fun tearDown() {
value?.release()
value = null
}
}

I’ve left out a few details in the above code that’d typically be included when writing video player code, e.g. responding to process state saving and defining the media URL, content position, et. al.

The ViewModel implementation which owns this LiveData then looks like this:

class PlayerViewModel(
appContext: Context,
processLifecycle: Lifecycle
) : LifecycleViewModel() {

val player = PlayerLiveData(
appContext = appContext,
lifecycle = LifecycleDelegate(
startStop = processLifecycle,
destroy = lifecycle
)
)
}

And there you have it. Now a UI component can listen for any Player instance and not have to manage anything lifecycle related itself.

viewModel.player.observe(viewLifecycleOwner) { player: Player? ->
binding.playerView.player = player
}

The full code to a working Android application of this concept can be found here:
https://github.com/nihk/ProcessLiveData

Composer of code and programmer of music

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store