Learn Jetpack Compose - Side Effects & Coroutines
Episode 10 of 23

Learn Jetpack Compose - Side Effects & Coroutines

This episode handles asynchronous work in Compose: LaunchedEffect, SideEffect, and DisposableEffect, coroutine integration with the UI, loading state patterns for network calls, and lifecycle-aware effects.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A composable is a pure function: given input, it produces UI. But real applications need to do things outside of composition — call the network, listen to listeners, write to files. This kind of work is called a side effect, and Compose provides dedicated APIs to run it safely.

Doing it carelessly — for example calling a coroutine directly inside a composable — introduces bugs instead, because composition can be re-run and cancelled at any time.

Episode 10 covers the effect family, coroutine integration, loading state patterns, and lifecycle-aware effects.

LaunchedEffect: Running Asynchronous Code

Effects That Follow Composition

LaunchedEffect runs a coroutine when the composable enters composition and cancels it when it leaves. This is the right place for initial data loading:

KotlinLaunchedEffect untuk load data
@Composable
fun ArtikelScreen(viewModel: ArtikelViewModel) {
    val state by viewModel.uiState.collectAsState()
 
    LaunchedEffect(Unit) {
        viewModel.muatArtikel()
    }
 
    when (val s = state) {
        is UiState.Memuat -> LoadingIndikator()
        is UiState.Sukses -> KontenArtikel(s.artikel)
        is UiState.Gagal -> PesanError(s.pesan)
    }
}

LaunchedEffect(Unit) runs once when composition starts. The key is in the parameter: if the key changes, the effect is cancelled and then re-run. Using when on a sealed class is the UI state pattern that will be unpacked in episode 20. To make sure lifecycle dependencies are wired up, check the dependency tree with ./gradlew :app:dependencies.

Restarting When the Key Changes

When a LaunchedEffect key changes, the old effect is cancelled and a new one runs. This pattern is useful for responding to parameter changes:

KotlinLaunchedEffect dengan key
LaunchedEffect(artikelId) {
    muatKomentar(artikelId)
}

When artikelId changes, the old coroutine is cancelled and a new load begins — preventing data from the old article from overwriting the new one.

SideEffect and DisposableEffect

SideEffect for Code Outside Composition

SideEffect runs after a successful composition — the right place to notify code outside Compose:

KotlinSideEffect
SideEffect {
    onSetStatusBar(warnaStatusBar)
}

SideEffect { ... } is called on every successful composition. Unlike LaunchedEffect, the code here is synchronous and must not suspend — useful for synchronizing with the view system.

DisposableEffect for Cleanup

DisposableEffect provides an onDispose block to clean up resources when the composable leaves composition:

KotlinDisposableEffect
DisposableEffect(Unit) {
    val listener = object : LocationListener {
        override fun onLocationChanged(p: Location) {
            posisiBaru(p)
        }
    }
    lokasiManager.tambahListener(listener)
 
    onDispose {
        lokasiManager.hapusListener(listener)
    }
}

onDispose guarantees the listener is removed when the composable is no longer used — preventing resource leaks and callbacks to a UI that is gone.

Coroutines and Lifecycle

rememberCoroutineScope

To launch coroutines from events like onClick, use rememberCoroutineScope. This scope is automatically cancelled when the composable leaves composition:

KotlinrememberCoroutineScope
val scope = rememberCoroutineScope()
 
Button(onClick = {
    scope.launch {
        simpanKeServer()
    }
}) { Text("Simpan") }

rememberCoroutineScope() wraps LaunchedEffect for ad-hoc use: the coroutine runs on the UI and is automatically cancelled when the composable disappears. Remember this pattern was already used for the Snackbar in episode 7.

collectAsStateWithLifecycle

To collect a Flow only while the UI is visible, use collectAsStateWithLifecycle from the lifecycle-runtime-compose library:

KotlinCollect sadar lifecycle
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.0")
KotlincollectAsStateWithLifecycle
val lokasi by viewModel.lokasiFlow.collectAsStateWithLifecycle()
Text("Posisi: $lokasi")

collectAsStateWithLifecycle stops collecting when the UI is not visible and resumes when it becomes visible again — avoiding wasted work in the background and becoming the official Android recommendation.

Loading State Patterns

One State, Many Conditions

Combine effects and state into a clean loading pattern: UiState carries one of three conditions, and the UI renders according to the condition. This pattern is also the foundation of MVVM and MVI in episode 20.

KotlinSealed UI state
sealed interface UiState<out T> {
    data object Memuat : UiState<Nothing>
    data class Sukses<T>(val data: T) : UiState<T>
    data class Gagal(val pesan: String) : UiState<Nothing>
}

UiState expresses UI conditions without booleans colliding with each other. With LaunchedEffect to trigger loading and when for rendering, the data flow stays easy to trace.

Closing

Episode 10 handled asynchronous work: LaunchedEffect for key-based loading, SideEffect for external synchronization, DisposableEffect with onDispose for cleanup, rememberCoroutineScope for ad-hoc events, and collectAsStateWithLifecycle for lifecycle-aware Flow collection.

Key takeaways:

  • LaunchedEffect runs a coroutine following composition.
  • Change the LaunchedEffect key to cancel and restart an effect.
  • DisposableEffect ensures cleanup with onDispose.
  • rememberCoroutineScope cancels coroutines when the composable disappears.
  • collectAsStateWithLifecycle stops collecting when the UI is not visible.
  • A sealed UiState neatly encapsulates loading, success, and error.

In episode 11 we will discuss data persistence and offline — Room and DataStore integration, state restoration with rememberSaveable and SavedStateHandle, offline-first patterns with repositories, and data-driven UI.