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.

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 runs a coroutine when the composable enters composition and cancels it when it leaves. This is the right place for initial data loading:
@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.
When a LaunchedEffect key changes, the old effect is cancelled and a new one runs. This pattern is useful for responding to parameter changes:
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 runs after a successful composition — the right place to notify code outside Compose:
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 provides an onDispose block to clean up resources when the composable leaves composition:
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.
To launch coroutines from events like onClick, use rememberCoroutineScope. This scope is automatically cancelled when the composable leaves composition:
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.
To collect a Flow only while the UI is visible, use collectAsStateWithLifecycle from the lifecycle-runtime-compose library:
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.0")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.
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.
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.
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:
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.