Jetpack Compose Simplified: Mastering Kotlin Scopes with Effect Handlers and Side Effects

Aman Garg
3 min readJul 28, 2024

--

Photo by Devin Avery on Unsplash

Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs. When working with Jetpack Compose, you’ll come across various scopes and effects. In this blog, we’ll explore them in simple terms with easy examples, divided into Suspend and Non-Suspend Effect Handlers, and Side Effect States.

Effect Handlers

Suspend Effect Handlers

These handlers are used when you need to perform tasks that involve suspending functions, such as network requests or long-running operations.

1. LaunchedEffect

LaunchedEffect is a composable function that runs a coroutine when a key changes. It is often used to start a coroutine that performs some work when a specific state changes.

Example

@Composable
fun MyScreen(userId: String) {
var userData by remember { mutableStateOf<User?>(null) }

LaunchedEffect(userId) {
// Simulate a network call
userData = fetchUserData(userId)
}

// UI based on userData
userData?.let {
Text(text = "User Name: ${it.name}")
} ?: run {
CircularProgressIndicator()
}
}

In this example, LaunchedEffect will fetch user data whenever the userId changes.

2. rememberCoroutineScope

rememberCoroutineScope provides a coroutine scope that is bound to the lifecycle of the 2. SideEffect.

Example

@Composable
fun MyButton() {
val scope = rememberCoroutineScope()

Button(onClick = {
scope.launch {
// Perform some long-running task
performTask()
}
}) {
Text("Start Task")
}
}

Here, rememberCoroutineScope gives us a coroutine scope to launch a task when the button is clicked.

Non-Suspend Effect Handlers

These handlers are used for tasks that do not involve suspending functions but still require lifecycle-aware handling.

1. DisposableEffect

DisposableEffect is used for side effects that need to be cleaned up when the key changes or the composable leaves the composition.

Example

@Composable
fun MySensorComponent() {
DisposableEffect(Unit) {
val sensor = SensorManager()
sensor.startListening()

onDispose {
sensor.stopListening()
}
}

// UI elements
Text("Listening to sensor data...")
}

In this example, the sensor starts listening when the composable enters the composition and stops when it leaves.

2. SideEffect

SideEffect is used to perform side effects that need to happen after a successful composition.

Example

@Composable
fun MySideEffectComponent(data: String) {
SideEffect {
Log.d("MySideEffectComponent", "Data: $data")
}

// UI elements
Text(text = data)
}

Here, SideEffect logs data every time the composable recomposes with new data.

Side Effect States

1. rememberUpdatedState

rememberUpdatedState helps to safely capture a value in a composable that might change.

Example

@Composable
fun TimerComponent(onTimeout: () -> Unit) {
val currentOnTimeout by rememberUpdatedState(onTimeout)

LaunchedEffect(Unit) {
delay(3000)
currentOnTimeout()
}

Text("Timer started...")
}

In this example, rememberUpdatedState ensures onTimeout is always up-to-date.

2. derivedStateOf

derivedStateOf is used to create a state that depends on other states.

Example

@Composable
fun MyDerivedStateComponent(items: List<String>) {
val itemCount by remember { derivedStateOf { items.size } }

Text("Item count: $itemCount")
}

Here, itemCount is derived from items and will update whenever items changes.

3. produceState

produceState helps create a state backed by a producer that manages its lifecycle.

Example

@Composable
fun ClockComponent() {
val time by produceState(initialValue = System.currentTimeMillis()) {
while (true) {
delay(1000L)
value = System.currentTimeMillis()
}
}

Text("Current time: ${Date(time)}")
}

In this example, produceState creates a state that updates every second to show the current time.

4. snapshotFlow

snapshotFlow converts a snapshot state to a flow.

Example

@Composable
fun MySnapshotFlowComponent() {
val count = remember { mutableStateOf(0) }

LaunchedEffect(Unit) {
snapshotFlow { count.value }
.collect { newValue ->
Log.d("MySnapshotFlowComponent", "Count: $newValue")
}
}

Button(onClick = { count.value++ }) {
Text("Increment")
}
}

Here, snapshotFlow creates a flow from count and logs the new value every time it changes.

Conclusion

When working with Jetpack Compose, choosing the right effect handler is crucial:

  • Suspend Effect Handlers (LaunchedEffect, rememberCoroutineScope): Use these when you need to perform tasks that involve suspending functions, like network requests or long-running operations. They are coroutine-based and handle asynchronous tasks efficiently.
  • Non-Suspend Effect Handlers (DisposableEffect, SideEffect): Use these for tasks that don’t involve suspending functions but still require lifecycle-aware handling, such as setting up and tearing down listeners or logging data.
  • Side Effect States (rememberUpdatedState, derivedStateOf, produceState, snapshotFlow): These help manage state changes and dependencies within your composables, ensuring your UI reacts correctly to state updates.

Understanding and using these tools effectively will help you write more efficient, maintainable, and reactive UI code in Jetpack Compose. Happy composing!

--

--