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 launchsuspend functions which are performing UI related tasks.
Page Content
Overview of the page content
- What is Side-effect?
- LaunchedEffect Side-effect API
LaunchedEffect
UnderTheHoodLaunchedEffect
ExampleLaunchedEffect
Applications- rememberCoroutineScope API
rememberCoroutineScope
UnderTheHoodrememberCoroutineScope
ExamplerememberCoroutineScope
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 functionLaunchedEffect
is taking a parameter and asuspend
function that must be executedLaunchedEffect
is passing current composable coroutine context passing in toLaunchedEffectImpl
withsuspend
function that will be executed, which shows the coroutine will be launched within the scope of the parent composable function.LaunchedEffectImpl
takessuspend
function as a block of code and lanches coroutine, canceling the previously running coroutine if it existsLaunchedEffect
expects at least one parameter to be passed, If you don’t want to pass any parameter you can either passnull 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.
- 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
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
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.
- 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
andrememberCoroutineScope
are side-effect APIs in order forside-effect
actions to be executed in a controlled and predictable manner.LaunchedEffect
executessuspend
functions in the scope of composable whereasrememberCoroutineScope
executes out of the scope of a composable but still scoped to be aware of composable life-cycle.LaunchedEffect
andrememberCoroutineScope
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 ButrememberCoroutineScope
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
andrememberCoroutineScope
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 🙂
— — — — — — — — — — —
This article was previously published on proandroiddev.com