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’slifecycle
- 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.
// MainViewModel | |
class NewsViewModel ( | |
private val newsRepository: NewsRepository = NewsRepositoryImpl() | |
) : ViewModel() { | |
init { | |
fetchNews() | |
} | |
private fun fetchNews() { | |
viewModelScope.launch { | |
newsRepository.fetchNews() | |
} | |
} | |
} | |
// MainScreen | |
@Composable | |
fun NewsScreen(viewModel: NewsViewModel = NewsViewModel()) { | |
LazyColumn{ | |
// showing list of | |
} | |
} |
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.
val lifecycleOwner = LocalLifecycleOwner.current | |
DisposableEffect(lifecycleOwner) { | |
val lifecycleEventObserver = LifecycleEventObserver { _, event -> | |
// event contains current lifecycle event | |
} | |
lifecycleOwner.lifecycle.addObserver(lifecycleEventObserver) | |
onDispose { | |
lifecycleOwner.lifecycle.removeObserver(lifecycleEventObserver) | |
} | |
} |
Job Offers
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.
@Composable | |
fun NewsScreen( | |
viewModel: NewsViewModel = NewsViewModel(), | |
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current | |
) { | |
var lifecycleEvent by remember { mutableStateOf(Lifecycle.Event.ON_ANY) } | |
DisposableEffect(lifecycleOwner) { | |
val lifecycleObserver = LifecycleEventObserver { _, event -> | |
lifecycleEvent = event | |
} | |
lifecycleOwner.lifecycle.addObserver(lifecycleObserver) | |
onDispose { | |
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) | |
} | |
} | |
LaunchedEffect(lifecycleEvent) { | |
if (lifecycleEvent == Lifecycle.Event.ON_RESUME) { | |
viewModel.fetchNews() | |
} | |
} | |
// will use to display news | |
LazyColumn { | |
// list of news | |
} | |
} |
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.
@Composable | |
fun rememberLifecycleEvent(lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): Lifecycle.Event { | |
var lifecycleEvent by remember { mutableStateOf(Lifecycle.Event.ON_ANY) } | |
DisposableEffect(lifecycleOwner) { | |
val lifecycleObserver = LifecycleEventObserver { _, event -> | |
lifecycleEvent = event | |
} | |
lifecycleOwner.lifecycle.addObserver(lifecycleObserver) | |
onDispose { | |
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) | |
} | |
} | |
return lifecycleEvent | |
} | |
@Composable | |
fun NewsScreen(viewModel: NewsViewModel = NewsViewModel()) { | |
val lifecycleEvent = rememberLifecycleEvent() | |
LaunchedEffect(lifecycleEvent) { | |
if (lifecycleEvent == Lifecycle.Event.ON_RESUME) { | |
viewModel.fetchNews() | |
} | |
} | |
// list of news | |
LazyColumn { | |
// list of news | |
} | |
} |
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
@Composable | |
fun DisposableEffectWithLifecycle( | |
onCreate: () -> Unit = {}, | |
onStart: () -> Unit = {}, | |
onStop: () -> Unit = {}, | |
onResume: () -> Unit = {}, | |
onPause: () -> Unit = {}, | |
onDestroy: () -> Unit = {}, | |
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current | |
) { | |
val currentOnCreate by rememberUpdatedState(onCreate) | |
val currentOnStart by rememberUpdatedState(onStart) | |
val currentOnStop by rememberUpdatedState(onStop) | |
val currentOnResume by rememberUpdatedState(onResume) | |
val currentOnPause by rememberUpdatedState(onPause) | |
val currentOnDestroy by rememberUpdatedState(onDestroy) | |
DisposableEffect(lifecycleOwner) { | |
val lifecycleEventObserver = LifecycleEventObserver { _, event -> | |
when (event) { | |
Lifecycle.Event.ON_CREATE -> currentOnCreate() | |
Lifecycle.Event.ON_START -> currentOnStart() | |
Lifecycle.Event.ON_PAUSE -> currentOnPause() | |
Lifecycle.Event.ON_RESUME -> currentOnResume() | |
Lifecycle.Event.ON_STOP -> currentOnStop() | |
Lifecycle.Event.ON_DESTROY -> currentOnDestroy() | |
else -> {} | |
} | |
} | |
lifecycleOwner.lifecycle.addObserver(lifecycleEventObserver) | |
onDispose { | |
lifecycleOwner.lifecycle.removeObserver(lifecycleEventObserver) | |
} | |
} | |
} | |
// News Screen | |
@Composable | |
fun NewsScreenWithDisposableEffectLifecycle(viewModel: NewsViewModel = NewsViewModel()) { | |
DisposableEffectWithLifecycle( | |
onResume = { viewModel.fetchNews() } | |
) | |
// list of news | |
LazyColumn { | |
// list of news | |
} | |
} |
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.
// Extension function | |
@Composable | |
fun <viewModel: LifecycleObserver> viewModel.observeLifecycleEvents(lifecycle: Lifecycle) { | |
DisposableEffect(lifecycle) { | |
lifecycle.addObserver(this@observeLifecycleEvents) | |
onDispose { | |
lifecycle.removeObserver(this@observeLifecycleEvents) | |
} | |
} | |
} | |
// ViewModel | |
class NewsViewModelLifeCycleObserver( | |
private val newsRepository: NewsRepository = NewsRepositoryImpl(), | |
): ViewModel(), DefaultLifecycleObserver { | |
override fun onResume(owner: LifecycleOwner) { | |
viewModelScope.launch { | |
newsRepository.fetchNews() | |
} | |
} | |
} | |
// News Scren | |
@Composable | |
fun NewsScreenWithViewModelAsLifecycleObserver( | |
viewModel: NewsViewModelLifeCycleObserver = NewsViewModelLifeCycleObserver() | |
) { | |
viewModel.observeLifecycleEvents(LocalLifecycleOwner.current.lifecycle) | |
// list of news | |
LazyColumn { | |
// list of news | |
} | |
} | |
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’slifecycle
are two different notions. - Every Composable has lifecycle owner
LocalLifeCycleOwner.current
which we can be use to add an observer for View’slifecycle
events DisposableEffect
provides way to observe and cleanup observer ononDispose
.LaunchedEffect
does not receiveON_CREATE
and firstON_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 🙂
This article was previously published on proandroiddev.com