Jetpack Compose — Making Composable lifecycle-aware

Saqib
ProAndroidDev
Published in
6 min readJun 12, 2023

--

In the story we will explore different ways to make Composable lifecycle-aware. We will also see differences between Composable’s lifecycle and View’s lifecycle.

We will go step by step exploring different solutions in order to find a better way to observe lifecycle events for a Composable in “Jetpack Compose-Way”.

To get an overview of the page content.

Page Content

  • Composable’s lifecycle?
  • View’s lifecycle?
  • Composable’s lifecycle vs View’s lifecycle
  • Making Composable lifecycle-aware
  • Take-aways
  • Github

Composable’s lifecycle?

The lifecycle a Composable is explained very nicely in the official documentation here, In this story I will briefly go over it.

Composable’s lifecycle is defined by following phases

  • Enter the Composition — When Jetpack compose runs the composables first time, It keeps track of Composables used to describe the UI and builds a tree-structure of all composables that’s called Composition.
  • Recomposition — It’s the phase when any state changes which eventually impacts the UI, Jetpack Compose smartly identifies those Composables and recomposes only them without the need to update all Composables.
  • Leave the Composition — It’s the last phase when the UI is no longer visible so it removes all resources consumed.

The following diagram (taken from official doc) visualises such phases nicely.

View’s lifecycle ?

View’s lifecycle is a very basic concept in any Mobile Development and its core paradigm on which many things depend within the UI Layer. It provides control on different states of a view to do the required work. The different such states are onCreate, onStart, onPause, onResume, onStop, onDestroy.

There are different use-cases when we have to react on such lifecycle events e.g if User is moved away from a page then there must be resources which you may not need any more and you can free them up, Or if User comes from background to foreground you might want to refetch latest information from backend to show updated content so on and so forth the list of such use-cases goes on.

Composable’s lifecycle vs View’s lifecycle

View’s lifecycle and Composable’s lifecycle are two different paradigms.

Jetpack Compose has introduced lifecycle of a Composable which has nothing to do with View’s lifecycle. Composable’s lifecycle is about creating UI components tree-structure, keeping track of state changes and providing efficient UI updates. Whereas View’s lifecycle is all about events triggering based on how User is interacting within our App/screen e.g moving to another screen, going to background, coming to foreground etc.

We still need to make our Composables lifecycle-aware to satisfy many use-cases. That means we have to listen for View’s lifecycle events and react to them to provide better user experience in the end.

Use-case

We want to refetch our App data when the user comes from background to foreground to fetch the latest information from backend and to update the UI with that latest information.

First let’s see how the code looks without implementing such behaviour.

The NewsScreen composable will display a list of news using LazyColumn.

We will not go into the UI implementation for News section and will assume that its implemented using Jetpack Compose

TheNewsViewModel is fetching data on initialisation, if user moves the App to background and then to foreground the News data will not be fetched again because on onResume the viewModelScope will not launch new coroutine automatically and fetchNews() will not execute.

To fulfill that case we have to make our Composable lifecycle-aware, observing for lifecycle events and When it’s onResume we must fetch News again.

Making Composable lifecycle-aware?

Every Composable has lifecycle owner LocalLifeCycleOwner.current which we will use to add an observer for View’s lifecycle events and react on them. We also need to make sure to remove that observer when the View destroys and Composable leaves the Composition. DisposableEffect side-effect API is an ideal choice here to add an observer and it provides onDispose block to cleanup.

If you are not familiar with DisposableEffect API or want to explore in detail, I wrote a detailed story about DisposableEffect API and its comparison with LaunchedEffect and remember(key). You can read from the link.

Below code shows how the DisposableEffect API implementation looks like after adding and removing lifecycle events observer.

Let’s update the code further to remember current lifecycle event into a state variable lifecycleEvent and extend the previous example to react on lifecycle event.

In the code above it remembered a state variable lifecycleEvent being updated from inside DisposableEffect. In NewsScreen composable added LaunchedEffect with lifecycleEvent as key and calling fetchNews inside lambda whenever the lifecycleEvent is ON_RESUME state. This will make the NewsScreen Composablelifecycle-aware . ( The code for NewsViewModel will stay the same which is exposing fetchNews method)

Now each time the View comes into Resume state it fetches the News again and View gets updated with latest content fulfilling our use-case of refreshing news coming from the background.

What if there are multiple Composables which need to be lifecycle-aware? Then let’s make this code reusable for other composables.

Let’s see the reusable code below.

The code inside NewsScreen composable gets simpler and more readable because all code which was observing lifecycle events is moved to a common Composable which is internally remembering the lifecycle state for that particular Composable. NewsScreen is just taking lifecycle state from rememberLifecycleEvent Composable and passing as key to LaunchedEffect which is refreshing news on ON_RESUME.

If you are not familiar with LaunchedEffect . I have written a detailed story about LaunchedEffect and rememberCoroutineScope Side-effect APIs, you can read from the link .

This solution has a problem: LaunchedEffect does not trigger on ON_CREATE and first ON_START lifecycle events, LaunchedEffect only starts listening from ON_RESUME lifecycle event and onward. Also LaunchedEffect is meant to run suspend functions which are related to UI.

One practical use-case will be to log analytics events when any screen gets open the first time. To achieve that we have to listen to ON_CREATE event in order to log analytics event, so we need to find a different solution to be able to react on ON_START/ON_CREATE lifecycle events.

We will use DisposableEffect API to listen for lifecycle events and react to them within the DisposableEffect API effect block. We also want to make the solution reusable so it can be incorporated into other Composables.

Let’s look into the code below

DisposableEffectWithLifecycle composable takes lambda parameters for all lifecycle events, observers lifecycle events and executes specific methods on each lifecycle event. DisposableEffectWithLifecycle encapsulate observing lifecycle events and cleanup when leaves the Composition. Its reusable solution and can be easily incorporated inside any other composable to make that composable lifecycle-aware.

It solves our problem and provides events on ON_CREATE and ON_START as well where our previous solution was failing.

It’s a reasonable solution but we can even make it better to move such code inside ViewModel, where our ViewModel will observe for lifecycle events and will react.

Making ViewModel Lifecycle-aware

To make ViewModel lifecycle-aware and in order to listen to lifecycle events for a particular Composable we have to pass Composable lifecycleOwner to the ViewModel.

To do that we will write an extension Composable function for ViewModel which will receive Composable lifecycle Owner LocalLifecycleOwner.current.lifecycle and will add observer and remove observer on onDispose block. The ViewModel will implement DefaultLifecycleObserver and will start receiving lifecycle events. Then on OnResume lifecycle event it will call fetchNews() method.

Let’s see that all in the code below.

ViewModel is observing for event changes and reacting on it.

Business logic is moved to the ViewModel and you can test your ViewModel on particular lifecycle state and checking the outcome on that state. Also We have less code in UI and one less method in ViewModel.

Take-aways

  • Composable’s lifecycle and View’s lifecycle are two different notions.
  • Every Composable has lifecycle owner LocalLifeCycleOwner.current which we can be use to add an observer for View’s lifecycle events
  • DisposableEffect provides way to observe and cleanup observer on onDispose.
  • LaunchedEffect does not receive ON_CREATE and first ON_START event.
  • Always minimise your UI code.

Sources

Github

Hope it was helpful

Looking forward to any other solution or recommendations.

Remember to follow and 👏 if you liked it :)

GitHub | LinkedIn | Twitter

--

--

Senior Mobile Engineer (Android & iOS) , Berlin | Sharing my development experience