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 doesn’t scale well and gives 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.

Rather than put all this lifecycle observing logic into the hands of a ViewModel, it can instead go directly into a LiveData subclass. This means the value of the LiveData subclass can be set based on the lifecycle callbacks of the lifecycles it is listening to. Note that I said lifecycles, as in the plural form; because the requirements outlined in the previous paragraph were to listen to the ProcessLifecycle but also when the UI component is navigated away from, not one but two lifecycles need to be listened to in this LiveData subclass. Below is the code for a generic class which facilitates that idea:

abstract class MultiLifecycleObserverLiveData<T>(
private val startStop: Lifecycle,
private val destroy: Lifecycle
) : LiveData<T>(), DefaultLifecycleObserver {

abstract fun createResource(): T
abstract fun tearDownResource()

override fun onActive() {
startStop.addObserver(this)
destroy.addObserver(this)
}

override fun onStart(owner: LifecycleOwner) {
if (owner.lifecycle != startStop) {
return
}

value = createResource()
}

override fun onStop(owner: LifecycleOwner) {
if (owner.lifecycle != startStop) {
return
}

tearDownResource()
}

override fun onDestroy(owner: LifecycleOwner) {
if (owner.lifecycle != destroy) {
return
}

startStop.removeObserver(this)
destroy.removeObserver(this)
tearDownResource()
}
}

The above class in a nutshell is this:

  • When any observers of the LiveData are active, start observing the two lifecycles provided via the constructor.
  • When onStart and onStop callbacks are hit, make sure these events came from the stopStart lifecycle. If so, respond accordingly to either create or tear down the resource.
  • When onDestroy is called back, make sure this event came from the destroy lifecycle. If so, tear down the resource and unsubscribe from listening to the lifecycles.

The implementation of this abstract class with respect to an ExoPlayer resource can then look like so:

class PlayerLiveData(
private val appContext: Context,
startStop: Lifecycle,
destroy: Lifecycle
) : MultiLifecycleObserverLiveData<Player>(startStop, destroy) {

override fun
createResource(): Player {
val player: ExoPlayer = ...
return player
}

override fun tearDownResource() {
value?.release()
}
}

I’ve left out a few details in the above code for the sake of brevity, e.g. responding to process state saving and defining the media URL, content position, et. al.

The startStop lifecycle, as you may have figured, comes from the ProcessLifecycleOwner. But who owns the destroy field’s lifecycle? It can’t be a UI Fragment/Activity because they will be destroyed and recreated after every configuration change and provide misleading onDestroy callbacks to our custom LiveData. Instead, it has to be the lifecycle of a retained Fragment because they survive configuration changes. Conveniently, the ViewModel which will end up being composed of the PlayerLiveData instance is inherently bound to a retained Fragment instance. ViewModel exposes an onCleared callback, too, to let us know when that retained Fragment is destroyed. By declaring ViewModel as a LifecycleOwner and marking its Lifecycle state as destroyed in the onCleared callback, we have ourselves a signal to the onDestroy callback of PlayerLiveDatas parent class. The ViewModel implementation ends up looking like this:

class PlayerViewModel(
application: Application
) : AndroidViewModel(application), LifecycleOwner {

// Alternatively expose this via a constructor dependency
private val processLifecycle: Lifecycle =
ProcessLifecycleOwner.get().lifecycle
    private val lifecycleRegistry = 
LifecycleRegistry(this)
.also { it.markState(Lifecycle.State.CREATED) }
    val player = PlayerLiveData(application, 
startStop = processLifecycle, destroy = lifecycle)

override fun getLifecycle(): Lifecycle = lifecycleRegistry

override fun
onCleared() {
super.onCleared()
lifecycleRegistry.markState(Lifecycle.State.DESTROYED)
}
}

And there you have it. Now a UI component can plug into all the expected ExoPlayer resource callbacks via a simple Observer such as:

viewModel.player.observe(viewLifecycleOwner, Observer {
player_view.player = it
}
)

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