Deep-dive into Compose Side-effect APIs LaunchedEffect and rememberCoroutineScope. Exploring differences and use-cases where we should use LaunchedEffect vs rememberCoroutineScope .

It’s a detailed guide about rememberCoroutineScope and LaunchedEffect the side-effect APIs in Jetpack Compose. These APIs are tailored built to execute suspend functions in their specific use-cases which we will explore in detail in this story.
Effect Apis
rememberCoroutineScope andLaunchedEffectshould only be used to launchsuspend functions which are performing UI related tasks.
Page Content
Overview of the page content
- What is Side-effect?
- LaunchedEffect Side-effect API
LaunchedEffectUnderTheHoodLaunchedEffectExampleLaunchedEffectApplications- rememberCoroutineScope API
rememberCoroutineScopeUnderTheHoodrememberCoroutineScopeExamplerememberCoroutineScopeApplications- Comparison b/w
LaunchedEffect &rememberCoroutineScope - Github project
What is Side-effect?
Side-effect is anything happening out of the scope of a composable function which eventually affects the composable function, it could be some state changes or anything happening on UI like user actions which has an effect on Composable. Both APIs are built to handle that effect in a controlled environment.
First Let’s explore LaunchedEffect in detail.
LaunchedEffect Side-effect API
LaunchedEffect is a composable function and it can only be executed from another composable function. LaunchedEffect takes at least one parameter and a suspend function. It executes that suspend function via launching a coroutine within the scope of the container composable. LaunchedEffect executes that suspend function as soon as it enters the Composition the first time and when one of its passed variables changes its value. When LaunchedEffect has to execute a new suspend function due to side-effect then it cancels the previously running coroutine and launches a new one with the new suspend function. LaunchedEffect also cancels the launched coroutine when it leaves the Composition itself. Coroutine is always launched within the scope of the container composable function.
LaunchedEffect UnderTheHood
Let’s look at the one of the functions declaration for LaunchedEffect
| @Composable | |
| @NonRestartableComposable | |
| @OptIn(InternalComposeApi::class) | |
| fun LaunchedEffect( | |
| key1: Any?, | |
| block: suspend CoroutineScope.() -> Unit | |
| ) { | |
| val applyContext = currentComposer.applyCoroutineContext | |
| remember(key1) { LaunchedEffectImpl(applyContext, block) } | |
| } |
| internal class LaunchedEffectImpl( | |
| parentCoroutineContext: CoroutineContext, | |
| private val task: suspend CoroutineScope.() -> Unit | |
| ) : RememberObserver { | |
| private val scope = CoroutineScope(parentCoroutineContext) | |
| private var job: Job? = null | |
| override fun onRemembered() { | |
| job?.cancel("Old job was still running!") | |
| job = scope.launch(block = task) | |
| } | |
| override fun onForgotten() { | |
| job?.cancel() | |
| job = null | |
| } | |
| override fun onAbandoned() { | |
| job?.cancel() | |
| job = null | |
| } | |
| } |
By looking at the code above following points to recap
LaunchedEffectis a composable function so can only be executed within another composable functionLaunchedEffectis taking a parameter and asuspendfunction that must be executedLaunchedEffectis passing current composable coroutine context passing in toLaunchedEffectImplwithsuspendfunction that will be executed, which shows the coroutine will be launched within the scope of the parent composable function.LaunchedEffectImpltakessuspendfunction as a block of code and lanches coroutine, canceling the previously running coroutine if it existsLaunchedEffectexpects at least one parameter to be passed, If you don’t want to pass any parameter you can either passnull orUnit . In this case I would choose to passUnit as parameter. If you would passUnit ornull as parameter thensuspendfunction will be executed exactly once and that in the Composition phase.
LaunchedEffect launches coroutine with the block of code in the scope of composable functions, the executed coroutine is canceled when LaunchedEffect leaves the composition or if any of the LaunchedEffect parameter changes.
LaunchedEffect Example
Let’s look at the code example below to understand some of the characteristics of LaunchedEffect
| @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") | |
| @OptIn(ExperimentalMaterial3Api::class) | |
| @Composable | |
| fun LaunchedEffectTestScreen ( | |
| snackbarHostState: SnackbarHostState, | |
| viewModel: LaunchedEffectTestViewModel | |
| ) { | |
| val snackbarCount = viewModel.snackbarCount.collectAsState() | |
| LaunchedEffect(snackbarCount.value) { | |
| Log.d("launched-effect","displaying launched effect for count ${snackbarCount.value}") | |
| try { | |
| snackbarHostState.showSnackbar("LaunchedEffect snackbar", "ok") | |
| } catch(e: Exception){ | |
| Log.d("launched-effect","launched Effect coroutine cancelled exception $e") | |
| } | |
| } | |
| Scaffold( | |
| snackbarHost = { SnackbarHost(hostState = snackbarHostState) } | |
| ) { | |
| Column { | |
| Text(text = "LaunchedEffect Test") | |
| } | |
| } | |
| } |
In the code example above LaunchedEffectTestScreen composable is using LaunchedEffect to show a snackbar the first time and when the passed parameter snackbarCount changes. The corresponding viewModel code is below.
| class LaunchedEffectTestViewModel : ViewModel() { | |
| private var _snackbarCount = MutableStateFlow(1) | |
| val snackbarCount: StateFlow<Int> get() = _snackbarCount | |
| init { | |
| viewModelScope.launch { | |
| var displayCount = 1 | |
| while (displayCount < 3) { | |
| delay(1000L) | |
| displayCount += 1 | |
| _snackbarCount.value = displayCount | |
| } | |
| } | |
| } | |
| } |
In ViewModel snackbarCount StateFlow starts with an initial value of 1. ViewModel is further launching a coroutine to update the snackbarCount StateFlow snackbarCount every second till max 3 times. As the value for snackbarCount will change LaunchedEffect will execute on every value change and a new coroutine will be launched with canceling the previous one. Log output of the above code will look like this below.
| D/launched-effect: displaying launched effect for count 1 | |
| D/launched-effect: launched Effect coroutine cancelled exception kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@28abbfe | |
| D/launched-effect: displaying launched effect for count 2 | |
| D/launched-effect: launched Effect coroutine cancelled exception kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@14b5985 | |
| D/launched-effect: displaying launched effect for count 3 |
It shows the LaunchedEffect executes the coroutine with snackbarCount 1 on launch and next time it launches a new coroutine with snackbarCount value of 2, cancelling the previous one. You can see JobCancellationException in the log for coroutine 1 and 2.
LaunchedEffect Applications
LaunchedEffect is usually effective when we want to execute a UI related task (suspend function) at the start during the Composition phase. But it will also execute when the passed state parameters values change. Following are some Applications of LaunchedEffect.
- Scrolling
LazyList to a specific position: In a chat application when a user first time loads the App or chat screen we want the user to see latest messages so we will scroll the chat messages to the bottom of the list, this can be achieved with the following code below usingLaunchedEffect.
LaunchedEffect(Unit, block = {
lazyListState.scrollToItem(messages.size - 1)
})
We are passing Unit as a parameter that means we only want to call this suspend block when the first time a user enters the screen i.e during the Composition phase. As soon as the user enters the screen it will scroll to the bottom of the list.
Github Project repo having this example is mentioned below.
GitHub – saqib-github-commits/BasicCompose
2. Perform animations as soon as the Composable is added to the Composition. There is an article about its usage with animations you can read from there -> Custom Canvas Animations in JetpackCompose
3. App Loading Screen : Showing a Loading screen on launch of the App is also a use case of LaunchedEffect . How can we achieve it? Lets see the code below.
We will create a LoadingScreen composable.
| @Composable | |
| fun LoadingScreen(onTimeout: () -> Unit) { | |
| Box( | |
| modifier = Modifier.fillMaxSize(), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| LaunchedEffect(Unit) { | |
| delay(5000L) | |
| onTimeout() | |
| } | |
| CircularProgressIndicator() | |
| } | |
| } |
Job Offers
LoadingScreen composable is showing a full screen composable with CircularProgressIndicator in the middle of the screen to show loading state in UI. LoadingScreen is also using the Api LauncedEffect passing Unit as parameter because we want to launch the passed block only when LoadingScreen enters the screen i.e during the Composition phase. LaunchedEffect is executing suspend function which is using delay to mimic backend response ( we don’t have backend yet ) and waiting for 5 seconds to display loading screen before calling onTimeOut method.
Now we will need to change the starter code in MainActivity to add a switch for LoadingScreen as below.
| var showLoading by remember { | |
| mutableStateOf(true) | |
| } | |
| if (showLoading) { | |
| LoadingScreen { showLoading = false } | |
| } else { | |
| val snackbarHostState = SnackbarHostState() | |
| LaunchedEffectTestScreen(snackbarHostState, LaunchedEffectTestViewModel()) | |
| } |
In MainActivity we are remembering the boolean state storing information about when to show LoadingScreen at first it’s value is true so LoadingScreen gets called passing in lambda which is turning the showLoading flag false — this method will get called inside LaunchedEffect within LoadingScreen after 5 seconds as we saw the code before. So after 5 seconds the flag showLoading turns false and It goes into the else part showing LaunchedEffectTestScreen.
Full code is available below.
GitHub – saqib-github-commits/JetpackComposeSuspendFunctions
4. Showing Snackbar message when the Network is not available: In real projects normally we would like to show a custom notification view within the page to show Network Status connected/offline and usually at the top of the page under the App Bar but for the sake of showing an example for LaunchedEffect I am using Snackbar for that.
Let’s look at the Composable.
| @Composable | |
| fun LaunchedEffectNetworkState( | |
| snackbarHostState: SnackbarHostState, | |
| viewModel: LaunchedEffectNetworkStateViewModel | |
| ) { | |
| val showNetworkUnavailable by viewModel.networkUnavailable.collectAsState() | |
| if (showNetworkUnavailable) { | |
| LaunchedEffect(Unit) { | |
| snackbarHostState.showSnackbar("Network Unavailable") | |
| } | |
| } | |
| Scaffold( | |
| snackbarHost = { SnackbarHost(hostState = snackbarHostState) } | |
| ) { | |
| Text(text = "Network State using LaunchedEffect") | |
| } | |
| } |
Composable is observing the state from viewModel in showNetworkUnavailable . If the value is true it will execute LaunchedEffect which is showing a snackbar message about network is not available And when value turns false then LaunchedEffect will leave Composition and cancel the coroutine launched before.
Let’s see ViewModel to see full picture.
| class LaunchedEffectNetworkStateViewModel: ViewModel() { | |
| private var _networkUnavailable = MutableStateFlow(false) | |
| val networkUnavailable get() = _networkUnavailable.asStateFlow() | |
| init { | |
| viewModelScope.launch { | |
| delay(2000L) | |
| _networkUnavailable.value = true | |
| } | |
| } | |
| } |
ViewModel is mimicking the effect of Network unavailable as we do not need to implement complete Network status listeners for the sake of example. ViewModel is exposing networkUnavailable StateFlow with initial value false and in the coroutine after 2 seconds it turns networkUnavailable value to true . As the value changes after 2 seconds the composable will show a snackbar message after 2 seconds executing a suspend function within LaunchedEffect.
That’s it related to LaunchedEffect . There are many other practical applications of LaunchedEffect But hope these helped in understanding LaunchedEffect in general.
rememberCoroutineScope Side-effect API
LaunchedEffect side-effect API is helpful to call suspend functions via coroutine during the Composition phase. But there are situations where we want to do some actions but not within the Composition but rather later in time e.g when user performs some actions on the UI, for that we need a scope to launch a coroutine where rememberCoroutineScope provides a coroutine scope bound with the scope of the composable where its being called to be aware of the life-cycle of the composable and cancels when it leaves the composition. With that scope we can call coroutines when we are not in the Composition i.e we can launch coroutine out of the scope of Composable in during user actions.
rememberCoroutineScope UnderTheHood
Let’s look at the function rememberCoroutineScope.
| @Composable | |
| inline fun rememberCoroutineScope( | |
| getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext } | |
| ): CoroutineScope { | |
| val composer = currentComposer | |
| val wrapper = remember { | |
| CompositionScopedCoroutineScopeCanceller( | |
| createCompositionCoroutineScope(getContext(), composer) | |
| ) | |
| } | |
| return wrapper.coroutineScope | |
| } |
A few points to verify as below
rememberCoroutineScopeis a composable function- It creates a coroutine scope scoped with the current composable so it will be aware of the composable life-cycle so will be automatically canceled as composable leaves the composition.
rememberCoroutineScope Example
Let’s look at the basic example showing usage of rememberCoroutineScope as in code below.
| @Composable | |
| fun RememberCoroutineScopeTestScreen ( ) { | |
| Column( | |
| modifier = Modifier.fillMaxSize(), | |
| verticalArrangement = Arrangement.Center, | |
| horizontalAlignment = Alignment.CenterHorizontally | |
| ) { | |
| val coroutineScope = rememberCoroutineScope() | |
| var counter by remember { mutableStateOf(0) } | |
| Text(text = counter.toString()) | |
| Spacer(modifier = Modifier.height(8.dp)) | |
| Button( | |
| onClick = { | |
| coroutineScope.launch { | |
| counter += 1 | |
| } | |
| } | |
| ) { | |
| Text(text = "Button") | |
| } | |
| } | |
| } |
Above code is showing a Text and a Button on the screen. We are taking a coroutine scope using rememberCoroutineScope and using it in the Button onClick event listener to launch a coroutine which increments the counter on every user button press event. onClick event listener is not in the scope of the Composition, it’s an event listener that’s why we need explicit coroutine scope to launch a coroutine outside the scope of a composable but it is scoped with the composable life-cycle.
rememberCoroutineScope Applications
There are many practical applications of rememberCoroutineScope . We will see some Applications which I have already used.
- LazyList with Go to Top/Bottom Buttons: There are usually scenarios where we have a list of data and buttons on the UI in order to scroll that list content to the Bottom or to the Top when the user performs those particular actions. Below code shows that case using
rememberCoroutineScope and launching coroutine to execute thosesuspend functions on theLazyList.
| // Button to Go To Bottom of the list | |
| Button(onClick = { | |
| coroutineScope.launch { lazyListState.animateScrollToItem(messages.size - 1) } | |
| }) { | |
| Text(text = "Go To Bottom") | |
| } | |
| // Button to Go To Top of the list | |
| Button(onClick = { | |
| coroutineScope.launch { lazyListState.animateScrollToItem(0) } | |
| }) { | |
| Text(text = "Go To Top") | |
| } |
2. ViewPager with Next and Prev Buttons: To scroll ViewPager on Next and Prev buttons actions is also an ideal application of rememberCoroutineScope as shown in code below.
| Button( | |
| enabled = prevButtonVisible.value, | |
| onClick = { | |
| val prevPageIndex = pagerState.currentPage - 1 | |
| coroutineScope.launch { pagerState.animateScrollToPage(prevPageIndex) } | |
| }, | |
| ) { | |
| Text(text = "Prev") | |
| } | |
| Button( | |
| enabled = nextButtonVisible.value , | |
| onClick = { | |
| val nextPageIndex = pagerState.currentPage + 1 | |
| coroutineScope.launch { pagerState.animateScrollToPage(nextPageIndex) } | |
| }, | |
| ) { | |
| Text(text = "Next") | |
| } |
Full code for the ViewPager Implementation example is below.
GitHub – saqib-github-commits/JetpackComposeViewPager
Comparison b/w LaunchedEffect & rememberCoroutineScope
To summarise below are important points in comparison.
LaunchedEffectandrememberCoroutineScopeare side-effect APIs in order forside-effectactions to be executed in a controlled and predictable manner.LaunchedEffectexecutessuspendfunctions in the scope of composable whereasrememberCoroutineScopeexecutes out of the scope of a composable but still scoped to be aware of composable life-cycle.LaunchedEffectandrememberCoroutineScopeboth APIs run in a life-cycle-aware manner and cancel the launched coroutines as soon as the composable where they are created leaves the composition.LaunchedEffectis usually used when we want to perform an action during Composition phase of the composable ( i.e as user enters the screen first time ) or if any state parameter passed to it changes ButrememberCoroutineScopeis used when we are not in the Composition, usually when user performs some action like a button press and we want to update the UI state in an effect to that action.LaunchedEffectandrememberCoroutineScopeshould only execute tasks related to UI. It should not violate unidirectional data flow.
Sources
Github project
The corresponding Github project can be find below
GitHub – saqib-github-commits/JetpackComposeSuspendFunctions
Hope it was helpful.
👏 if you liked it and follow for more stories 🙂
— — — — — — — — — — —
This article was previously published on proandroiddev.com



