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.
ViewModel
s 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