React to Lifecycle state changes in Compose

Fabio Catinella
4 min readMar 12, 2023

--

Jetpack Compose is amazing and everyone loves it, still, if you have worked with it, you may have needed to react to lifecycle state changes, maybe to migrate an old app from XML.

Well, It happened to me as well, and looks like we are not alone.

Casus belli

Working on app that should have read an NFC tag just to display some data, we came up with this solution:

//adapter : NfcAdapter

DisposableEffect(key1 = true, effect = {
adapter?.enableForegroundDispatch(
activity,
getPendingIntent(activity),
null,
null
)
onDispose {
adapter?.disableForegroundDispatch(activity)
}
}
)

Can you spot the mistake?

As you probably know, the Side Effect DisposableEffect runs for any new unique value of key1 and calls its onDispose block if key1 changes or if the DisposableEffect leaves the composition.

Now look at what the NFC documentation says:

https://developer.android.com/guide/topics/connectivity/nfc/advanced-nfc

Also looking inside NFCAdapter.java we can find this piece of documentation:

At this point is pretty clear that the function disableForegroundDispatch must be called during the onPause of the activity that created the NFCAdapter.
Unfortunately using a DisposableEffect is not enough, as written above the onDispose block is called when the effect leaves the composition and this is unlikely to happen during an onPause. In particular it is usual that it’s called during the onStop.
That’s why our solution doesn’t work.

Observe a lifecyle state

Well, now we know what we should do (call disableForegroundDispatch during the onPause) but, how could we do that? As already mention at the beginning, Compose doesn’t offer a simple way to observe Lifecyle state changes. We have to build our own way to do so.

We would like to have an Effect that runs code blocks based on the lifecycle changes. We could name this effect (just as someone suggested), LifecycleEventEffect.

@Composable
fun LifecycleEventEffect(
onCreate: () -> Unit = {},
onStart: () -> Unit = {},
onResume: () -> Unit = {},
onPause: () -> Unit = {},
onStop: () -> Unit = {},
onDestroy: () -> Unit = {},
) {...}

Now that we have chosen the name we are half way through our journey, the only thing to do is to write the actual implementation of this Effect.

Implementation

Luckily for us the androidx.lifecycle offers the convenient class Lifecycle that standing to the documentation allows us to observe lifecyle events.

Lifecycle.java

Thanks Google

Wouldn’t be great if we could get an instance of Lifecycle that represent the LifecycleOwner of where my Composable function is running?

Well, it already exists and it’s already part of Jetpack Compose!

import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle

@Composable
fun customComponent(){
...
val lifecycle : Lifecycle = LocalLifecycleOwner.current.lifecycle
...
}

Let’s put things together

First we need to get our friend Lifecycle and then we must attach to it a custom LifecycleEventObserver in order to achieve exactly what we wanted. Also, it would nice to stop observing it when this custom Effect we’ve just created leaves the composition. Sounds familiar?
DisposableEffect is just what we need.

So to make a recap, we wanted to :

  • Get the current lifecycle of the Android Component that is executing our Composable. -> LocalLifecycleOwner.current.lifecycle
  • Observe it and react when changes -> LifecycleEventObserver
  • Stop to do it when our Composable is out of the composition -> DisposableEffect

At last, behold LifecycleEventEffect.

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

@Composable
fun LifecycleEventEffect(
onCreate: () -> Unit = {},
onStart: () -> Unit = {},
onResume: () -> Unit = {},
onPause: () -> Unit = {},
onStop: () -> Unit = {},
onDestroy: () -> Unit = {},
) {
val lifecycle = LocalLifecycleOwner.current.lifecycle

val observer = remember {
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> onCreate()
Lifecycle.Event.ON_START -> onStart()
Lifecycle.Event.ON_RESUME -> onResume()
Lifecycle.Event.ON_PAUSE -> onPause()
Lifecycle.Event.ON_STOP -> onStop()
Lifecycle.Event.ON_DESTROY -> onDestroy()
Lifecycle.Event.ON_ANY -> {}
}
}
}

DisposableEffect(key1 = true) {
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
}
}
}

The greatest part is that is working!

Get back to NFC problem

Now that we have created this new Effect, we can use it in the code I posted to finally fix the issue that brought us here:

// adapter : NFCAdapter

LifecycleEventEffect(
onResume = {
adapter?.enableForegroundDispatch(
activity,
getPendingIntent(activity),
null,
null
)
},
onPause = {
adapter?.disableForegroundDispatch(activity)
},
)

That’s all for today! Let me know what you think about this solution and if you found a better one.

Bye!

References:

--

--