Blog Infos
Author
Published
Topics
,
Published

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 and LaunchedEffect should only be used to launch suspend functions which are performing UI related tasks.

Page Content

Overview of the page content

  • What is Side-effect?
  • LaunchedEffect Side-effect API
  • LaunchedEffect UnderTheHood
  • LaunchedEffect Example
  • LaunchedEffect Applications
  • rememberCoroutineScope API
  • rememberCoroutineScope UnderTheHood
  • rememberCoroutineScope Example
  • rememberCoroutineScope Applications
  • 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

  • LaunchedEffect is a composable function so can only be executed within another composable function
  • LaunchedEffect is taking a parameter and a suspend function that must be executed
  • LaunchedEffect is passing current composable coroutine context passing in to LaunchedEffectImpl with suspend function that will be executed, which shows the coroutine will be launched within the scope of the parent composable function.
  • LaunchedEffectImpl takes suspend function as a block of code and lanches coroutine, canceling the previously running coroutine if it exists
  • LaunchedEffect expects at least one parameter to be passed, If you don’t want to pass any parameter you can either pass null or Unit . In this case I would choose to pass Unit as parameter. If you would pass Unit or null as parameter then suspend function 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.

  1. 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 using LaunchedEffect.
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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Jetpack Compose: Drawing without pain and recomposition

This is a talk on recomposition in Jetpack Compose and the myths of too many calls it is followed by. I’ll briefly explain the reasons behind recompositions and why they are not as problematic as…
Watch Video

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jobs

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

4Showing 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

  • rememberCoroutineScope is 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.

  1. 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 those suspend functions on the LazyList.
// 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.

  • LaunchedEffect and rememberCoroutineScope are side-effect APIs in order for side-effect actions to be executed in a controlled and predictable manner.
  • LaunchedEffect executes suspend functions in the scope of composable whereas rememberCoroutineScope executes out of the scope of a composable but still scoped to be aware of composable life-cycle.
  • LaunchedEffect and rememberCoroutineScope both 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.
  • LaunchedEffect is 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 But rememberCoroutineScope is 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.
  • LaunchedEffect and rememberCoroutineScope should 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 🙂

— — — — — — — — — — —

GitHub | LinkedIn | Twitter

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu