MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Compose is fast by design, but it's easy to write composables that recompose far more than necessary. Here's how to identify recomposition problems, understand why they happen, and fix them with the right patterns.
On this page
Compose's recomposition model is simple in theory: when state changes, composables that read that state recompose. In practice, understanding exactly what triggers recomposition — and how to minimize it — requires knowing the specifics.
Compose tracks which state objects a composable reads during composition. When that state changes, the composable recomposes.
// This composable reads `count` — it recomposes when count changes
@Composable
fun CountDisplay(count: Int) {
Text(text = "Count: $count")
}
// This composable reads nothing from state — never recomposes unnecessarily
@Composable
fun StaticLabel(label: String) {
Text(text = label)
}The problem: Compose also recomposes composables when their parent recomposes, unless it can determine the parameters haven't changed (stability).
Compose marks a type as stable if it can determine that two instances with the same properties will always produce the same composition output. For stable types, Compose can skip recomposition when the parameters haven't changed.
Stable by default:
IntStringBooleanFloatUnstable by default:
varList<T>Enable the Recomposition Counts layout inspector in Android Studio:
Layout Inspector → Live Updates → Enable "Composition Tracking"
Composables that recompose excessively will show high counts. Alternatively, add logging:
@Composable
fun TaskItem(task: Task) {
SideEffect {
println("TaskItem recomposed for ${task.id}")
}
// ...
}If
TaskItem// Unstable — `tasks` is a mutable List
data class TaskUiState(
val tasks: MutableList<Task> = mutableListOf(),
val isLoading: Boolean = false
)
// Stable — all fields are immutable
data class TaskUiState(
val tasks: List<Task> = emptyList(), // Still a mutable List type but...
val isLoading: Boolean = false
)
// More explicitly stable:
@Immutable // or @Stable
data class TaskUiState(
val tasks: ImmutableList<Task> = persistentListOf(),
val isLoading: Boolean = false
)Use
kotlinx.collections.immutableImmutableListimplementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")Read state as low in the tree as possible:
// Bad — entire TaskListScreen recomposes when ANY task changes
@Composable
fun TaskListScreen(tasks: List<Task>, searchQuery: String) {
Column {
SearchBar(query = searchQuery) // This triggers recomposition of Column
LazyColumn {
items(tasks) { task ->
TaskItem(task = task) // These also recompose
}
}
}
}
// Better — separate reads so recomposition is targeted
@Composable
fun TaskListScreen(viewModel: TaskListViewModel) {
// Each of these reads its own state independently
val tasks by viewModel.tasks.collectAsStateWithLifecycle()
val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle()
Column {
SearchBar(query = searchQuery, onQueryChange = viewModel::onQueryChange)
TaskList(tasks = tasks)
}
}Every time a parent recomposes, it creates new lambda instances. Lambdas as composable parameters may trigger unnecessary recomposition if the child thinks its parameters changed:
// Bad — new lambda instance on every parent recomposition
@Composable
fun TaskListScreen(viewModel: TaskListViewModel) {
val tasks by viewModel.tasks.collectAsStateWithLifecycle()
TaskList(
tasks = tasks,
onComplete = { id -> viewModel.completeTask(id) } // New lambda every time
)
}
// Better — stable lambda reference
@Composable
fun TaskListScreen(viewModel: TaskListViewModel) {
val tasks by viewModel.tasks.collectAsStateWithLifecycle()
val onComplete = remember { { id: String -> viewModel.completeTask(id) } }
TaskList(tasks = tasks, onComplete = onComplete)
}Or move the lambda to the ViewModel:
// cleanest — ViewModel reference is stable
TaskList(tasks = tasks, onComplete = viewModel::completeTask)When your list items can be reordered or replaced, provide stable keys:
LazyColumn {
items(
items = tasks,
key = { task -> task.id } // Stable key prevents unnecessary recomposition on reorder
) { task ->
TaskItem(task = task)
}
}Without
keyWhen state is derived from other state, use
derivedStateOf// Without derivedStateOf: recomposes on EVERY scroll position change
val showFab = lazyListState.firstVisibleItemIndex > 0
// With derivedStateOf: only recomposes when the boolean result changes
val showFab by remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > 0 }
}The Layout Inspector in Android Studio 2024+ shows real-time recomposition counts per composable. Numbers in green are healthy. Numbers climbing rapidly or unexpectedly high are problems.
Use it specifically on the screens with most user interaction — list scrolling, typing in search fields, real-time updates.
List<T>ImmutableListkotlinx.collections.immutableremember { lambda }viewModel::functionkeyLazyColumnderivedStateOfSudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read