65

I need to automatically refresh an Android Compose screen when the app returns to the foreground.

I have an that requires permissions and location services.

If the user has switched any of these off a list is drawn of the items that need to be changed. When the user goes to Settings and the app returns to the foreground I would like the list to refresh to reflect the changes.

I am using Compose and Compose navigation. I have looked and I can't figure out the equivalent of onResume lifecycle event that could be used to trigger the refresh.

Any ideas would be gratefully received as I am at a loss.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
William
  • 1,255
  • 2
  • 10
  • 9

7 Answers7

83

I came up with this:

@Composable
fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) {
    val eventHandler = rememberUpdatedState(onEvent)
    val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
    
    DisposableEffect(lifecycleOwner.value) {
        val lifecycle = lifecycleOwner.value.lifecycle
        val observer = LifecycleEventObserver { owner, event ->
            eventHandler.value(owner, event)
        }

        lifecycle.addObserver(observer)
        onDispose {
            lifecycle.removeObserver(observer)
        }
    }
}

It seems to work just fine. But there may be some issues in some cases so be careful.
It is also possible that there is some redundant code.

Usage:

OnLifecycleEvent { owner, event ->
    // do stuff on event
    when (event) {
        Lifecycle.Event.ON_RESUME -> { /* stuff */ }
        else                      -> { /* other stuff */ }
    }
}
JojoIV
  • 992
  • 1
  • 9
  • 12
49

I slightly improved @JojoIV answer and made it flat usage without callback like you observe LiveData in compose what @Abdelilah El Aissaoui answered

@Composable
fun Lifecycle.observeAsState(): State<Lifecycle.Event> {
    val state = remember { mutableStateOf(Lifecycle.Event.ON_ANY) }
    DisposableEffect(this) {
        val observer = LifecycleEventObserver { _, event ->
            state.value = event
        }
        this@observeAsState.addObserver(observer)
        onDispose {
            this@observeAsState.removeObserver(observer)
        }
    }
    return state
}

and then usage

@Composable
fun SomeComposable() {
   val lifecycleState = LocalLifecycleOwner.current.lifecycle.observeAsState()
   val state = lifecycleState.value
   // or val lifecycleState by LocalLifecycleOwner.current.lifecycle.observeAsState()
  // will re-render someComposable each time lifecycleState will change
}
Jakoss
  • 4,647
  • 2
  • 26
  • 40
Arsenius
  • 4,972
  • 4
  • 26
  • 39
  • This answer was useful when using resume and pause only, but not start and stop. – bam bam Nov 08 '22 at 05:44
  • @bambam why is that? – Joshua King Nov 17 '22 at 21:50
  • Print (the `state` before return) and (the `event` of `LifecycleEventObserver`) in the first snippet . In my case the former only called RESUME and PAUSE. I'm not sure, but it seems that there is an omission by passing through the compose state rather than passing the event right away. If it doesn't reproduce, I'll do some more testing. @JoshuaKing – bam bam Nov 18 '22 at 02:39
27

example from google site

@Composable
fun HomeScreen(
  lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
  onStart: () -> Unit, // Send the 'started' analytics event
  onStop: () -> Unit   // Send the 'stopped' analytics event
) {
    // Safely update the current lambdas when a new one is provided
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    // If `lifecycleOwner` changes, dispose and reset the effect
    DisposableEffect(lifecycleOwner) {
        // Create an observer that triggers our remembered callbacks
        // for sending analytics events
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        // Add the observer to the lifecycle
        lifecycleOwner.lifecycle.addObserver(observer)

        // When the effect leaves the Composition, remove the observer
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    /* Home screen content */
}

Full description of how it works in google site https://developer.android.com/jetpack/compose/side-effects#disposableeffect

Sergei S
  • 2,553
  • 27
  • 36
16

Using oficial API (requires Lifecycle Runtime Compose 2.7.0):

implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0-alpha01")

The official way to detect Lifecycle.State changes would be using the following code snippet:

val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()

LaunchedEffect(lifecycleState) {
    // Do something with your state
    // You may want to use DisposableEffect or other alternatives
    // instead of LaunchedEffect
    when (lifecycleState) {
        Lifecycle.State.DESTROYED -> {}
        Lifecycle.State.INITIALIZED -> {}
        Lifecycle.State.CREATED -> {}
        Lifecycle.State.STARTED -> {}
        Lifecycle.State.RESUMED -> {}
    }
}

collectStateAsState is a convenience extension function that collects State changes by using Lifecycle's new property currentStateFlow. So the code above would be the same as doing:

val lifecycleOwner = LocalLifecycleOwner.current
val state by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()

LaunchedEffect(state) {
    // Do something with your state
    // You may want to use DisposableEffect or other alternatives 
    // instead of LaunchedEffect
}

Remember to import androidx.compose.runtime.getValue to access directly the Lifecycle.State.

As mentioned by @HåkonSchia, you can also use variations of DiposableEffect that also take in consideration the Lifecycle:

LifecycleStartEffect(Unit) {
    // Do something on start or launch effect

    onStopOrDispose {
        // Do something on stop or dispose effect
    }
}

LifecycleResumeEffect(Unit) {
    // Do something on resume or launch effect

    onPauseOrDispose {
        // Do something on pause or dispose effect
    }
}

Old answer:

Compose is not aware of state changes like onPause or onResume, you have to handle it using the parent activity's methods.

An example would be a LiveData instance in your activity that updates each time onResume is executed and observe it as a state in your main parent composable.

Let's take a look at the following example:

class MainActivity : AppCompatActivity() {
    // Use whatever type your prefer/require, this is just an example
    private val exampleLiveData = MutableLiveData("")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // Your main composable
            MyApplicationTheme {
                // Save the state into a variable otherwise it won't work
                val state = exampleLiveData.observeAsState()
                Log.d("EXAMPLE", "Recomposing screen - ${state.value}")

                Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }

    override fun onResume() {
        super.onResume()

        // Save whatever you want in your live data, this is just an example
        exampleLiveData.value = DateTimeFormatter.ISO_INSTANT.format(Instant.now())
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApplicationTheme {
        Greeting("Android")
    }
}

As you can see in this example, I have a LiveData property in my activity that containts a String. Whenever onResume is executed the property is updated with the new timestamp and the observing composable is recomposed.

Abdelilah El Aissaoui
  • 4,204
  • 2
  • 27
  • 47
  • 1
    Thanks @Abdelilah. That's what I thought but I really hoped there would be something built in to Compose to help with this situation. We have built our app with Compose navigation too so there is only one Activity and one Fragment in the entire app. This is the first real issue that we have encountered but your solution is similar to what we were thinking but using the timestamp is nicer. Thank you. – William Mar 10 '21 at 08:59
  • Please include all the dependencies that we need to include in order to make this thing work. I'm unable to find 2.7.0 version. Add the content of your build.gradle file. Thank you! – nutella_eater Apr 18 '23 at 21:34
  • @nutella_eater as mentioned in the warning, the release is not out yet. The Android team usually releases a new version of the lifecycle libraries every 2 weeks but this time seems to be a bit later, since it's been almost 4 weeks, probably because it includes quite some changes. The proposed solution is based on this commit in case you are interested https://github.com/androidx/androidx/commit/b75b447af3d27aed4b4bc5a826e6e92cd0aaff34 – Abdelilah El Aissaoui Apr 18 '23 at 21:56
  • 1
    Seems like 2.7.0 also includes convenience functions like `LifecycleStartEffect()` and `LifecycleResumeEffect()` https://github.com/androidx/androidx/blob/androidx-main/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt – Håkon Schia Jun 07 '23 at 10:58
  • Thanks @HåkonSchia, I've added it as well as updating the dependencies and the code snippet. – Abdelilah El Aissaoui Jul 26 '23 at 17:35
5

Here is solution in 2023

Create utility function:

@Composable
fun rememberLifecycleEvent(lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): Lifecycle.Event {
    var state by remember { mutableStateOf(Lifecycle.Event.ON_ANY) }
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            state = event
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
    return state
}

Use it in your screen composable:

val lifecycleEvent = rememberLifecycleEvent()
LaunchedEffect(lifecycleEvent) {
     if (lifecycleEvent == Lifecycle.Event.ON_RESUME) {
        // initiate data reloading
     }
}

Source: Jetpack Compose with Lifecycle-Aware Composables

tasjapr
  • 632
  • 4
  • 13
4

i changed @ojoIV code to this (if your composable code is in Activity)

@Composable
fun ComponentActivity.LifecycleEventListener(event: (Lifecycle.Event) -> Unit) {
    val eventHandler by rememberUpdatedState(newValue = event)
    val lifecycle = this@LifecycleEventListener.lifecycle
    DisposableEffect(lifecycle) {
        val observer = LifecycleEventObserver { _, event ->
            eventHandler(event)
        }
        
        lifecycle.addObserver(observer)
        
        onDispose {
            lifecycle.removeObserver(observer)
        }
    }
}

usage

LifecycleEventListener(event = { lifecycleEvent ->
    when (lifecycleEvent ) {
        Lifecycle.Event.ON_CREATE -> {}
        Lifecycle.Event.ON_START -> {}
        Lifecycle.Event.ON_RESUME -> {}
        Lifecycle.Event.ON_PAUSE -> {}
        Lifecycle.Event.ON_STOP -> {}
        Lifecycle.Event.ON_DESTROY -> {}
        else -> return@LifecycleEventListener
    }
})
EunhaEonnie
  • 223
  • 2
  • 4
1

You can use the following helper method:

@Composable
fun LifecycleEffect(event: Lifecycle.Event, skip: Int = 1, handler: () -> Unit) {
    val lifecycleOwner = LocalLifecycleOwner.current
    var skipped by remember { mutableIntStateOf(0) }
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, e ->
            if (e == event) {
                if (skipped < skip) {
                    skipped += 1
                } else {
                    handler()
                }
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

the observer receives the first event by default, this allows skipping it, it's also pretty convenient to use:

LifecycleEffect(Lifecycle.Event.ON_START) {
    //...
}
Ahmed Mourad
  • 193
  • 1
  • 2
  • 18