Jetpack Compose with Lifecycle-Aware Composables
Bridging between Composable and View’s lifecycles
If you come with an Android background, you should be familiar with the notion of lifecycles. Jetpack Compose introduces a new paradigm to build declarative UI using Composables. They come with a simplified lifecycle including three phases:
- The entering phase.
- The recomposition phase.
- The exiting phase.
The official documentation describes in detail how a Composable behaves during its full lifecycle.
Now, you may be tempted to think the Composable’s lifecycle replaces the View’s lifecycle. Even though the naming aligns, we’re talking about two different notions.
For instance, even if you’d build a pure Compose application, your Composables may need to interact with the View’s lifecycle. That would be the case if you’d have a Composable refreshing some data when the application comes in the foreground.
Yet, the Composable knows nothing about the View’s lifecycle. You’ll need to bridge those two universes to bring this awareness to your Composables.
The Resumed Composable Use Case
Let’s assume we’re building a pure Compose application. Including a screen Composable ProductsScreen
. This screen injects a ViewModel — via Hilt for instance — to fetch the products. We’d have the following code:
The ViewModel launches a coroutine when created and delegates the fetching operation to a repository.
Now, we’d like to refresh our products each time the user displays this screen. This can happen when the user goes to the background and re-enters the foreground — provided the system didn’t kill the application. Or if you’d navigate to a different screen then navigate back to the ProductsScreen
.
You’ll soon realize our sample code doesn’t satisfy our requirements. The fetching will only be active during the first time the Composable appears. That’s because the ViewModel cancels the coroutine as the Composable exits the composition tree, but doesn’t relaunch it when re-entering the composition phase.
One solution would be to rely on the Composable’s entering phase to fetch your products. You could expose the fetch method from your ViewModel and call it from your Composable. Remember to call this method using a side effect. Otherwise, you’ll fetch your products not only when the composable gets visible but at each recomposition when its state will change!
If you’d like to call it once when the Composable enters its composition phase, you can use a LaunchEffect
and Unit
as a key — or true
, as long as it doesn’t change during the composition.
We haven’t checked all boxes though. Using the entering phase will help us refresh our products when navigating back to this screen. That’s not the case when the user has put the application in the background.
It matters to distinguish how Composable and View’s lifecycles work. But it’s also valuable to understand that you can combine them.
Bringing the View’s Lifecycle To a Compose World
In a pure Compose application, you must create an Activity as an entry point. We can rely on it to get the View’s lifecycle and pass it down to our Composable.
But passing down some events from the holding Activity to the Composable gets tedious. Especially when you have several parents Composables in between. Fortunately, we can directly make any Composables lifecycle-aware.
If you’re not familiar with lifecycle-aware components in Android, I’ve covered it with a similar use case in this article.
A similar configuration works with Jetpack Compose. But there exists a smarter, more “Compose-like” way to achieve it. Each Composable exposes the current lifecycle owner where you can retrieve the View’s lifecycle. Using a DisposableEffect
side-effect, you can store the View’s lifecycle latest event and react upon it while safely detaching its observer when the Composable exits the composition tree.
All of it would quickly become boilerplate if more Composables would turn lifecycle-aware. We can write a Composable extension method:
With this configuration, you can react to the View’s lifecycle with a few additional lines of code.
Lifecycle-Aware ViewModel for Jetpack Compose
In the previously linked article, I’ve mentioned how vital ViewModel plays in your application scalability. Especially when some business logic is tightly bound to your View’s lifecycle.
Using Compose doesn’t absolve you from doing so — even though it makes the separation of concern easier.
With the above solution, the View still holds the responsibility of reacting to lifecycle events and requesting the fetch to the ViewModel. Ideally, the ViewModel should take care of it.
Using the same technique, we create another Composable extension method to let any LifecycleOwner
observe the View’s lifecycle. Then, we let the ViewModel implement the DefaultLifecycleObserver
interface. We can move all the logic into the ViewModel!
Wrapping up
We’ve seen that, with a simple use case, relying solely on the Composable’s lifecycle won’t make the cut. With the lifecycle-aware components library and a DisposableEffect
, you can transfer the lifecycle events to the ViewModel.
Also, the testing process remains unchanged compared to my previous article. This means we can keep our lifecycle-aware ViewModel logic regardless if injected by a View or a Composable. Only the lifecycle binding differs — and it’s even more straightforward with Compose with the extension method!
I hope this will help you in your migration process towards Jetpack Compose while mitigating the impact on your code. Happy coding!