Understanding Side Effects in Jetpack Compose
1. LaunchedEffect
LaunchedEffect is used to run suspend functions within a composable's lifecycle. It starts a coroutine when the composable enters the composition and cancels it when the composable is removed or when its key changes. This is ideal for triggering UI updates or background tasks that depend on specific state changes.
package com.example.compose.effects
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@Composable
fun AlertDisplayScreen() {
val snackbarManager = remember { SnackbarHostState() }
var isAlertActive by remember { mutableStateOf(false) }
LaunchedEffect(key1 = isAlertActive) {
if (isAlertActive) {
snackbarManager.showSnackbar("System alert triggered. Please verify.")
delay(1500)
isAlertActive = false
}
}
Scaffold(
topBar = { TopAppBar(title = { Text("Effect Demonstration") }) },
snackbarHost = { SnackbarHost(snackbarManager) }
) { innerPadding ->
Box(
modifier = Modifier.fillMaxSize().padding(innerPadding),
contentAlignment = Alignment.Center
) {
Button(onClick = { isAlertActive = true }) {
Text("Trigger Alert")
}
}
}
}
2. rememberCoroutineScope
rememberCoroutineScope provides a CoroutineScope that is tied to the composition lifecycle. It is primarily used to launch coroutines in response to user interactions, such as button clicks, where you need to call suspend functions that cannot be executed directly from a @Composable function.
package com.example.compose.effects
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DashboardView() {
val coroutineScope = rememberCoroutineScope()
val messageHost = remember { SnackbarHostState() }
val panelController = rememberDrawerState(initialValue = DrawerValue.Closed)
ModalNavigationDrawer(
drawerState = panelController,
drawerContent = {
ModalDrawerSheet {
Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
Text(text = "Navigation Panel Content")
}
}
}
) {
Scaffold(
snackbarHost = { SnackbarHost(hostState = messageHost) },
topBar = {
TopAppBar(
title = { Text("Application Dashboard") },
navigationIcon = {
IconButton(onClick = {
coroutineScope.launch {
panelController.animateTo(DrawerValue.Open)
}
}) {
Icon(Icons.Default.Menu, contentDescription = "Open Menu")
}
}
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
coroutineScope.launch {
messageHost.showSnackbar("Operation completed successfully")
}
},
text = { Text("Execute") }
)
}
) { innerPadding ->
Box(
modifier = Modifier.fillMaxSize().padding(innerPadding),
contentAlignment = Alignment.Center
) {
Text(text = "Main Content Area")
}
}
}
}
3. rememberUpdatedState
When running long-running operations like LaunchedEffect, capturing the initial value of a parameter can lead to stale closures. rememberUpdatedState ensures that your coroutine always references the latest value of a parameter without restarting the effect when that parameter changes.
package com.example.compose.effects
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@Composable
fun CountdownTimer(onFinish: () -> Unit) {
val currentCallback by rememberUpdatedState(onFinish)
LaunchedEffect(key1 = Unit) {
Log.d("ComposeSideEffects", "Timer sequence initiated")
repeat(5) { iteration ->
delay(1000L)
Log.d("ComposeSideEffects", "Elapsed: ${iteration + 1} seconds")
}
currentCallback()
}
}
@Composable
fun TimerConfigurationScreen() {
val actionA = { Log.d("ComposeSideEffects", "Timer finished: Action A") }
val actionB = { Log.d("ComposeSideEffects", "Timer finished: Action B") }
var activeAction by remember { mutableStateOf(actionA) }
Column {
Button(onClick = {
activeAction = if (activeAction === actionA) actionB else actionA
}) {
Text("Switch Completion Action")
}
Spacer(modifier = Modifier.height(16.dp))
CountdownTimer(onFinish = activeAction)
}
}
4. DisposableEffect
DisposableEffect is designed for side effects that require cleanup when a composable leaves the composition or its keys change. It is common used for registering and unregistering event listeners, starting and stopping hardware sensors, or managing external callbacks.
package com.example.compose.effects
import android.util.Log
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcher
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun NavigationInterceptor(
dispatcher: OnBackPressedDispatcher,
onBackRequested: () -> Unit
) {
val callback = remember {
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Log.d("ComposeSideEffects", "Back press intercepted")
onBackRequested()
}
}
}
DisposableEffect(key1 = dispatcher) {
Log.d("ComposeSideEffects", "Registering back press listener")
dispatcher.addCallback(callback)
onDispose {
Log.d("ComposeSideEffects", "Removing back press listener")
callback.remove()
}
}
}
@Composable
fun InterceptorControlScreen(dispatcher: OnBackPressedDispatcher) {
var isEnabled by remember { mutableStateOf(false) }
Row {
Text(text = if (isEnabled) "Interception Active" else "Interception Disabled")
Spacer(modifier = Modifier.width(16.dp))
Switch(checked = isEnabled, onCheckedChange = { isEnabled = it })
}
if (isEnabled) {
NavigationInterceptor(
dispatcher = dispatcher,
onBackRequested = { Log.d("ComposeSideEffects", "Custom back action executed") }
)
}
}
5. SideEffect
SideEffect executes a block of code after every successful composition. It is primarily used to bridge Compose state with non-Compose code, such as analytics libraries or advertising SDKs, ensuring that external systems stay synchronized with the current UI state.
package com.example.compose.effects
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun AnalyticsTracker(eventId: String) {
var logged by remember { mutableStateOf(false) }
SideEffect {
if (!logged) {
Log.d("ComposeSideEffects", "Event tracked: $eventId")
logged = true
}
}
}
6. ProduceState
produceState converts non-Compose state, such as network responses or external data sources, into Compose State. It launches a coroutine that updates a State object, automatically triggering recomposition when the value changes. This is highly effective for loading states.
package com.example.compose.effects
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BrokenImage
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp
object MediaService {
suspend fun fetchAsset(id: String): ByteArray {
kotlinx.coroutines.delay(1500)
return if (id == "valid") "mock_image_data".toByteArray() else throw IllegalStateException("Network failure")
}
}
sealed class FetchStatus<T> {
object Pending : FetchStatus<Nothing>()
object Failed : FetchStatus<Nothing>()
data class Completed<T>(val data: T) : FetchStatus<T>()
}
@Composable
fun requestMediaResource(
resourceId: String,
service: MediaService
): State<FetchStatus<ByteArray>> {
return produceState(initialValue = FetchStatus.Pending, key1 = resourceId) {
Log.d("ComposeSideEffects", "Fetching resource on background thread")
try {
val result = service.fetchAsset(resourceId)
value = FetchStatus.Completed(result)
} catch (e: Exception) {
value = FetchStatus.Failed
}
}
}
@Composable
fun ResourceLoaderDemo() {
val assetIds = remember { listOf("asset_01", "asset_02", "asset_03") }
var currentIndex by remember { mutableStateOf(0) }
val fetchResult = requestMediaResource(
resourceId = assetIds[currentIndex],
service = MediaService
)
Column {
Button(onClick = {
currentIndex = (currentIndex + 1) % assetIds.size
}) {
Text(text = "Load Next Resource (${currentIndex + 1})")
}
when (val currentStatus = fetchResult.value) {
is FetchStatus.Pending -> CircularProgressIndicator()
is FetchStatus.Failed -> androidx.compose.foundation.Image(
imageVector = Icons.Default.BrokenImage,
contentDescription = "Load failed",
modifier = androidx.compose.ui.Modifier.size(100.dp)
)
is FetchStatus.Completed -> Text("Data loaded successfully: ${currentStatus.data.size} bytes")
}
}
}
7. derivedStateOf
derivedStateOf creates a state that is derived from other states. It only triggers recomposition when the computed value actual changes, optimizing performance by skipping unnecessary UI updates when intermediate states fluctuate without affecting the final result.
package com.example.compose.effects
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
fun String.matchesCriteria(keywords: List<String>): Boolean =
keywords.any { keyword -> this.contains(keyword, ignoreCase = true) }
@Composable
fun TaskRow(taskName: String, modifier: Modifier = Modifier) {
Text(
text = taskName,
textAlign = TextAlign.Start,
modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PriorityFilterView(
criticalKeywords: List<String> = listOf("urgent", "blocker", "critical")
) {
val taskList = remember { mutableStateListOf<String>() }
val urgentTasks = remember(taskList, criticalKeywords) {
derivedStateOf {
taskList.filter { it.matchesCriteria(criticalKeywords) }
}
}
var inputText by remember { mutableStateOf("") }
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
TextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f)
)
Button(onClick = {
if (inputText.isNotBlank()) {
taskList.add(inputText.trim())
inputText = ""
}
}, modifier = Modifier.padding(start = 8.dp)) {
Text("Append Task")
}
}
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(urgentTasks.value) { task ->
TaskRow(taskName = task, modifier = Modifier.background(Color(0xFFFFEBEE)))
}
items(taskList) { task ->
TaskRow(taskName = task)
}
}
}
}
@Composable
fun DerivedStateDemo() {
PriorityFilterView()
}
8. snapshotFlow
snapshotFlow converts Compose State objects into a Kotlin Flow. This allows developers to leveraeg the powerful operators provided by Kotlin Coroutines to filter, debounce, or map state changes, making it ideal for observing complex UI interactions like scrolling or input validation.
package com.example.compose.effects
import android.util.Log
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@Composable
fun ScrollThresholdMonitor() {
val listConfiguration = rememberLazyListState()
LazyColumn(state = listConfiguration) {
items(count = 200) { index ->
Text(text = "List Item #$index", modifier = androidx.compose.ui.Modifier.padding(8.dp))
}
}
LaunchedEffect(key1 = listConfiguration) {
Log.d("ComposeSideEffects", "Monitoring scroll state initialized")
snapshotFlow { listConfiguration.firstVisibleItemIndex }
.filter { threshold -> threshold > 50 }
.distinctUntilChanged()
.collect { currentIndex ->
Log.d("ComposeSideEffects", "User scrolled past threshold: $currentIndex")
}
}
}