Blog Infos
Author
Published
Topics
Published

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 onCreateonStartonPauseonResumeonStoponDestroy.

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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

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’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

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu