Using rememberCoroutineScope() vs LaunchedEffect
Asked Answered
P

8

75

Context

In Jetpack compose, we have the option of using rememberCoroutineScope() as well as using the LaunchedEffect composable in order to use coroutines / run suspend functions (show snackbars etc).

The convention I've adopted so far is to remember a single coroutine scope at the top of my compose tree, and pass it down via function arguments to places where it is needed. This vaguely seems like a good practice, but on the other hand it's adding extra noise to my function signatures.

Questions

  1. Are there any reasons for preferring the use of LaunchedEffect over rememberCoroutineScope() inside composable functions?
  2. Is it worth the effort to only create / remember a coroutine scope once per compose tree, or should I just call rememberCoroutineScope() in each function where a coroutine is actually launched?
Protoxylem answered 4/3, 2021 at 11:24 Comment(7)
I know #1 was discussed on Kotlinlang Slack recently, but Slack's search is pathetic, so I can't find it. For #2, the issue is not "worth the effort" but what the right answer is for the coroutine. For example, you write AboutScreen() and have in there a Text() that shows the number of seconds since the app was built, and you use a coroutine to update the state for that Text(). The user visits the about screen, then leaves and uses other portions of your app for an hour.Oscillogram
Should your every-second coroutine be running through that whole hour, even though the about screen is not in your composition? If the answer is "heck, no", then you need a CoroutineScope that is scoped to the relevant composable, and that would be rememberCoroutineScope() in that composable. If for some reason you do need that coroutine to keep running, you need a scope that matches the desired lifetime. This is no different than any other coroutine scope decision: it's not about what's easy, but what is the proper lifetime for the coroutine to execute.Oscillogram
Thanks, I'll try to look at Kotlinlang Slack. For #2, all the screens/fragments in my app have separate compose trees, so the lifetime is at most as long as a user spends on one screen. But I was thinking more about short lived things, like showing snackbars and running animations. Should I be reusing the same coroutine scope for many different short lived things, or create a new one each time? It sounds like what you're saying is that I should make the scopes no larger than necessary, which leans towards creating/remembering the coroutine scopes separately in each place they're used.Protoxylem
"I should make the scopes no larger than necessary" -- within reason, yes. Going back to your question, "Is it worth the effort to only create / remember a coroutine scope once per compose tree", the answer is "no". Coroutine scopes are cheap, and remembering things is cheap, so there is no need to artificially restrict your use of them. You can instead focus on what the business logic says their lifetime should be.Oscillogram
Thank you very much. I'd accept this as the answer if you want to make it into one.Protoxylem
Given the title of your question, my comments won't really be an answer. You really need the LaunchedEffect side of matters for that.Oscillogram
proandroiddev.com/… - Blog post on LaunchedEffect and rememberCoroutineScopePentlandite
M
81

Leaving my understanding here:

Question 1: LaunchedEffect should be used when you want that some action must be taken when your composable is first launched/relaunched (or when the key parameter has changed). For example, when you want to request some data from your ViewModel or run some sort of animation...
rememberCoroutineScope on the other hand, is specific to store the Coroutine scope allowing the code to launch some suspend function... imho, the only relation between them is that you can also use a LaunchedEffect to launch a coroutine...

Question 2: As you can see in the docs, rememberCoroutineScope will keep the reference of the coroutine's scope in a specific point of the composition. Therefore, if a given composable is removed from the recomposition, that coroutine will be cancelled automatically. For instance, you have the following composable calls A -> B -> C. If you remember the coroutine scope in C and it is removed from the composition, the coroutine is automatically cancelled. But if you remember from A, pass the scope through B and C, use this scope in C, and then C is removed, the coroutine will continue running (because it was remembered in A)...

Minimal answered 12/3, 2021 at 13:38 Comment(3)
Addendum: I'm using AS Arctic Fox Canary 12 and now seeing explicit warnings against using coroutineScope.launch { ... } inside a composable function: "Calls to launch should happen inside a LaunchedEffect and not composition." (The lint name is "CoroutineCreationDuringComposition"). So for what it's worth, this gives an 'official' directive that my previous way of using a coroutineScope for everything was not the preferred way.Protoxylem
rememberCoroutineScope is to launch a coroutine outside a composable within a composition-aware scope. For inside composables, coroutines should be launched with LaunchedEffectAmerce
If you write coroutineScope.launch { ... } in a Composable function, it will launch on every recomposition!Janice
F
11

Use rememberCoroutineScope() when you are using coroutines and need to cancel and relaunch the coroutine after an event

Use LaunchedEffect() when you are using coroutines and need to cancel and relaunch the coroutine every time your parameter changes and it isn’t stored in a mutable state.

Added code here.

 val scope = rememberCoroutineScope()
 Row(modifier = Modifier
        .clickable {  scope.launch { /* call suspend */ } } {
    LaunchedEffect(key1 = key){
      /* call suspend when entering or key changed */
      }
 }

An interesting thing is I saw many lines of code as the below.

 val scope = rememberCoroutineScope()
 Row(modifier = Modifier){
     LaunchedEffect(key1 = key){
        scope.launch{
         /* call suspend when entering or key changed */
        }
     }
 }

However, I don't think we need an extra scope in this case.

 Row(modifier = Modifier){
    LaunchedEffect(key1 = key){
     /* call suspend when entering or key changed */
    }
 }
Freehearted answered 3/2, 2022 at 16:40 Comment(0)
B
6

LaunchedEffect: run suspend functions in the scope of a composable

To call suspend functions safely from inside a composable, use the LaunchedEffect composable. When LaunchedEffect enters the Composition, it launches a coroutine with the block of code passed as a parameter. The coroutine will be cancelled if LaunchedEffect leaves the composition.

rememberCoroutineScope: obtain a composition-aware scope to launch a coroutine outside a composable

As LaunchedEffect is a composable function, it can only be used inside other composable functions. In order to launch a coroutine outside of a composable, but scoped so that it will be automatically canceled once it leaves the composition, use rememberCoroutineScope

More from here.

Bedmate answered 22/4, 2022 at 0:20 Comment(0)
B
4

rememberCoroutineScope is a composable function that returns a CoroutineScope bound to the point of the Composition where it's called. The scope will be cancelled when the call leaves the Composition. If you create your own coroutineScope instead of remember you might get MonotonicFrameClock is not available in this CoroutineContext error as in question here.

LaunchedEffect is a remember under the hood with coroutineScope which runs when it enters composition and when any of its keys change.

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}

LaunchedEffect is good for launching animations, snackbar, scolls any default suspending function Compose has also another very useful usages is triggering some events without user interaction

for instance executing a callback only when reaching a certain state without user interactions as in this question

LaunchedEffect(statementsByYear != null) {

   if (statementsByYear != null) {
         onDataAcquired(statementsByYear!!) 
   } 
}

LaunchedEffect gets triggered on composition but as in question since value is null if block condition is not met then when statementsByYear is set by api statementsByYear != null changes from true to false and if block condition is met and you run the statement.

Or only once inside a Composable using help of ViewModel saving a flag as in this question.

block of LaunchedEffect(keys) is invoked on composition and when any keys change. If you set keys from your ViewModel this LaunchedEffect will be launched and you can create a conditional block that checks same flag to be true that is contained in ViewModel

LaunchedEffect(mViewModel.isLaunched) {
    if(!mViewModel.isLaunched) {
          mViewMode.iniBilling(context as Activity)
          mViewMode.isLaunched = true
    }
}

Also conditional blocks enter composition when conditions are met so by wrapping LaunchedEffect with if block you can define when it will enter and exit composition which means canceling job its coroutineScope might be running as in this answer

if (count > 0 && count <5) {
    // `LaunchedEffect` will cancel and re-launch if
    // `scaffoldState.snackbarHostState` changes
    LaunchedEffect(scaffoldState.snackbarHostState) {
        // Show snackbar using a coroutine, when the coroutine is cancelled the
        // snackbar will automatically dismiss. This coroutine will cancel whenever
        // if statement is false, and only start when statement is true
        // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
        scaffoldState.snackbarHostState.showSnackbar("count $count")
    }
}

This block will enter composition when count is bigger than 0 and stay in composition while count is less than 5 but since it's LaunchedEffect it will trigger once but if count reaches 5 faster than Snackbar duration Snackbar gets canceled because block leaves composition.

Bravura answered 21/7, 2022 at 19:1 Comment(0)
C
0

LaunchedEffect launches a coroutine every time it faces new keys. It's a composable so it's bound to the tree and will be destroyed and cancelled when removed from tree.

rememberCoroutineScope creates a coroutine scope bound to the composable. Then you have a Kotlin coroutine scope that you can use anywhere to launch coroutines. The scope will be destroyed when the composable that called it is removed from tree.

Where you call rememberCoroutineScope and where you use the created scope, depends on your needs and the required lifecycle for the coroutines you launch.

Crafton answered 8/12, 2023 at 19:35 Comment(0)
B
0

LaunchedEffect is launched once when the initial composition takes place and It's protected to be launched over and over again against number of potential recompositions (unless it's parameterised and one of the parameters happened to change). Therefore it's good for running animations, snackbar, etc.

LaunchedEffect is also Composable itself and launches a coroutine behind the scenes.

rememberCoroutineScope also gives you a coroutine too but it's not a Composable. And that's for a good reason. Otherwise, it would run on every recomposition regardless. Therefore it's usable, for example, within event callbacks (i.e.: onClick) to run suspend functions.

Busby answered 11/4, 2024 at 11:58 Comment(0)
P
-1

LaunchedEffect as a composable function can only be used inside of another composable function; on the other hand, rememberCoroutineScope allows you to launch a coroutine outside of a composable function

Check this link for further details: https://developer.android.com/jetpack/compose/side-effects

Psychodynamics answered 13/12, 2023 at 7:36 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.