React to Lifecycle state changes in Compose
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:
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.
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!