
When you first start working with Jetpack Compose, everything feels smooth: you write a Composable function, it renders UI, and the state updates automatically when data changes. But soon you’ll run into situations where you need to do something outside the UI system — like showing a Toast, making a network call, logging analytics, or updating the database.
These “actions outside Compose” are called side-effects.
Imagine you’re cooking in your kitchen. You follow a recipe (your composable function), but sometimes you need to do things that aren’t directly related to cooking — like turning on the exhaust fan, setting a timer, or ordering groceries online. These are side-effects: actions that happen outside your main task but are necessary for everything to work smoothly.
What is a Side-Effect in Jetpack Compose?
A side-effect is any action that affects something outside of the Composable function.
Composable functions are meant to be pure functions — they take input (state) and return UI. But real-world apps need to do more than just draw UI.
Example of a side-effect:
- Showing a Toast message
- Saving data to SharedPreferences
- Making a network call
- Logging an event to analytics
- Starting a Coroutine that continues even after recomposition
Without proper handling, these actions can cause problems like:
- Running multiple times unnecessarily
- Memory leaks
- Crashes when UI is destroyed
That’s why Jetpack Compose gives us Side-Effect APIs.
Why Do We Need Side-Effects?
Compose functions are designed to be pure and recomposable. This means:
- They can run multiple times
- They can run in any order
- They should produce the same UI for the same inputs
But real apps need to do more than just show UI. We need to manage these “impure” operations safely, and that’s where side-effect handlers come in.
Side-Effect APIs in Jetpack Compose
Here are the most important ones you’ll use:
- LaunchedEffect
- rememberCoroutineScope
- DisposableEffect
- SideEffect
- produceState
- derivedStateOf
- rememberUpdatedState
- snapshotFlow
Let’s explore each side-effect handler with examples.
1. LaunchedEffect
- Runs a coroutine when key changes or when Composable is first launched.
- Useful for one-time tasks like API calls, animations, or showing a Toast.
Real-World Analogy: Like setting up a recurring alarm when you start a new job. When your job changes, you reset the alarm.
When to Use It?
Use LaunchedEffect when you need to run suspend functions (coroutines) in response to UI events or state changes.
Example: Loading Data from API
@Composable
fun UserProfileScreen(userId: String) {
var userProfile by remember { mutableStateOf<User?>(null) }
var isLoading by remember { mutableStateOf(true) }
// This runs when userId changes
LaunchedEffect(userId) {
isLoading = true
userProfile = fetchUserFromAPI(userId) // API call
isLoading = false
}
if (isLoading) {
CircularProgressIndicator()
} else {
Text("Welcome, ${userProfile?.name}")
}
}
What’s happening?
- When the screen opens or
userIdchanges, the API call runs - The loading indicator shows while fetching
- Once data arrives, the UI updates
2. rememberCoroutineScope
- Gives you a CoroutineScope tied to the Composable lifecycle.
- You can launch coroutines in response to user actions (like button clicks).
Real-World Analogy: Like having a phone in your pocket to make calls whenever needed, not just when someone calls you.
When to Use It?
When you need to launch coroutines from non-composable code (like click handlers).
Example: Handling Button Clicks
@Composable
fun SaveDataScreen() {
val scope = rememberCoroutineScope()
var saveStatus by remember { mutableStateOf("") }
Button(onClick = {
// Launch coroutine from click handler
scope.launch {
saveStatus = "Saving..."
delay(2000) // Simulate network call
saveDataToServer()
saveStatus = "Saved!"
}
}) {
Text("Save")
}
Text(saveStatus)
}
3. DisposableEffect
- Used when you need to set up something when Composable enters composition and clean it up when it leaves.
Real-World Analogy: Like renting a car. You pick it up (start), use it, and return it (clean up) when done.
When to Use It?
When you need to set something up AND clean it up when the composable leaves the screen or when keys change.
Example: Registering a Listener
@Composable
fun LocationTracker() {
val context = LocalContext.current
var currentLocation by remember { mutableStateOf("Unknown") }
DisposableEffect(Unit) {
val locationListener = LocationListener { location ->
currentLocation = "${location.latitude}, ${location.longitude}"
}
// Register listener
locationManager.requestLocationUpdates(locationListener)
// Clean up when composable leaves
onDispose {
locationManager.removeUpdates(locationListener)
println("Location listener removed")
}
}
Text("Current Location: $currentLocation")
}
Why is this important? If you don’t remove the listener, it keeps running even after the screen closes, wasting battery and memory!
4. SideEffect
- Runs after every successful recomposition.
- Useful when you want to update something in a non-Compose object.
Real-World Analogy: Like updating your mirror every time you change your outfit to make sure they match.
When to Use It?
When you need to publish Compose state to non-Compose code on every successful recomposition.
Example: Updating Analytics State
@Composable
fun ShoppingCart(items: List<Item>) {
val totalPrice = items.sumOf { it.price }
// Update external analytics object every time total changes
SideEffect {
AnalyticsManager.currentCartValue = totalPrice
}
Text("Total: $$totalPrice")
}
Important: SideEffect runs after every successful composition. Use it sparingly!
5. produceState
- Convert suspending functions (like API calls) into State directly.
Real-World Analogy: Like a translator converting a foreign language book into your language, page by page as you read.
When to Use It?
When you need to convert non-Compose asynchronous data sources into Compose State.
Example: Flow to State
@Composable
fun TemperatureDisplay(sensorFlow: Flow<Float>) {
val temperature by produceState(initialValue = 0f, sensorFlow) {
sensorFlow.collect { newTemp ->
value = newTemp
}
}
Text("Temperature: ${temperature}°C")
}
6. derivedStateOf
- Creates a calculated state that only updates when dependencies change.
- Helps in performance optimization.
Real-World Analogy: Like a calculator that automatically updates the total when you change the numbers.
When to Use It?
When you have expensive calculations that should only run when specific dependencies change.
Example: Filtered List
@Composable
fun ContactList(contacts: List<Contact>) {
var searchQuery by remember { mutableStateOf("") }
// Only recalculates when searchQuery or contacts change
val filteredContacts by remember {
derivedStateOf {
if (searchQuery.isEmpty()) {
contacts
} else {
contacts.filter { it.name.contains(searchQuery, ignoreCase = true) }
}
}
}
SearchBar(value = searchQuery, onValueChange = { searchQuery = it })
LazyColumn {
items(filteredContacts) { contact ->
Text(contact.name)
}
}
}
Performance Tip: Without derivedStateOf, the filtering would happen on every recomposition, even when nothing changed!
7. rememberUpdatedState
- Keeps latest value/lambda reference without restarting effect.
- Useful in long-running effects (timers, listeners) where callback changes often.
Real-World Analogy: Like bookmarking a live-updating scoreboard instead of a screenshot. You always see the current score.
When to Use It?
When you have a long-running effect that should use the latest value of a changing parameter.
Example: Timeout with Latest Callback
@Composable
fun TimerWithCallback(
onTimeout: () -> Unit
) {
// This captures the latest callback
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(5000)
currentOnTimeout() // Uses the latest version
}
}
// Usage
TimerWithCallback(
onTimeout = {
println("Current count: $currentCount") // Latest count
}
)
Why is this needed? Without it, the callback might use outdated values from when the effect first launched.
8. snapshotFlow
- Converts Compose state reads into a
Flow. - Useful for Flow operators: debounce, distinctUntilChanged, collectLatest.
- Collect inside
LaunchedEffect.
Real-World Analogy: Like a security camera converting real-world movements into a video stream.
When to Use It?
When you need to use Compose state in Flow-based APIs.
Example: Reacting to State Changes
@Composable
fun SearchBox() {
var query by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
snapshotFlow { query }
.debounce(300) // Wait 300ms after user stops typing
.filter { it.length >= 3 } // Only search if 3+ characters
.collect { searchQuery ->
performSearch(searchQuery)
}
}
TextField(
value = query,
onValueChange = { query = it }
)
}
Common Mistakes to Avoid
❌ Mistake 1: Using LaunchedEffect Without Keys
// BAD - Runs only once
LaunchedEffect(Unit) {
loadData()
}
// GOOD - Runs when userId changes
LaunchedEffect(userId) {
loadData(userId)
}
❌ Mistake 2: Not Cleaning Up Resources
// BAD - Listener never removed
LaunchedEffect(Unit) {
listener = createListener()
registerListener(listener)
}
// GOOD - Properly cleaned up
DisposableEffect(Unit) {
val listener = createListener()
registerListener(listener)
onDispose { unregisterListener(listener) }
}
❌ Mistake 3: Calling Suspend Functions Directly
// BAD - Won't compile
@Composable
fun LoadData() {
val data = fetchData() // Can't call suspend function here
}
// GOOD
@Composable
fun LoadData() {
var data by remember { mutableStateOf<Data?>(null) }
LaunchedEffect(Unit) {
data = fetchData()
}
}
Quick Remember

Job Offers
Summary
Side-effects in Jetpack Compose might seem complex at first, but they follow simple patterns:
- LaunchedEffect — For starting things
- DisposableEffect — For starting AND stopping things
- SideEffect — For syncing things
- rememberCoroutineScope — For responding to user actions
- rememberUpdatedState — For always using fresh values
- produceState — For converting to State
- derivedStateOf — For calculating efficiently
- snapshotFlow — For converting to Flow
Think of them as tools in a toolbox. Once you understand what each tool does, you’ll know exactly which one to reach for based on your needs.
Thank you for reading. 🙌🙏✌.
Found this helpful? Don’t forgot to clap 👏 and follow me for more such useful articles about Android development and Kotlin or buy us a coffee here ☕
If you need any help related to Mobile app development. I’m always happy to help you.
Follow me on:
This article was previously published on proandroiddev.com



