Compose Performance
Recomposition Rules
- Composable functions can recompose at any time — no side effects in body
- Recomposition skips unchanged parameters — make parameters stable
- Use
rememberfor expensive computations - Use
derivedStateOffor derived state
Stability
What makes a type stable?
- Primitives (
Int,String,Boolean) @Immutableor@Stableannotated classesdata classwith all stable properties
// Domain models passed to Composable should be stable
@Immutable
data class ItemUiModel(val id: String, val name: String, val avatarUrl: String)
// ViewState: data class is stable if all fields are stable
// Note: Flow fields cause instability but are acceptable for PagingData
data class SampleViewState(
val items: Flow<PagingData<ItemUiModel>>? = null, // Flow is NOT stable, but OK here
) : IViewState
Lambda Stability
// GOOD: Lambda hoisted to stateful Screen - created once, passed down
SampleScreenInternal(
item -> viewModel.onTriggerEvent(SampleViewEvent.OnItemClick(item)) }
)
// BAD: Lambda created inline causes recomposition
ItemCard(
viewModel.onTriggerEvent(SampleViewEvent.OnItemClick(it)) }
// New lambda instance every recomposition!
)
Defer State Reads
// BAD: Reading state in composition phase
Box(modifier = Modifier.offset(y = scrollState.value.dp))
// GOOD: Defer read to layout phase with lambda
Box(modifier = Modifier.offset { IntOffset(0, scrollState.value) })
remember & derivedStateOf
val sortedItems = remember(items) { items.sortedBy { it.name } }
val hasItems by remember { derivedStateOf { items.isNotEmpty() } }
PagingData + Compose (Correct Pattern)
@Composable
fun SampleScreenInternal(viewModel: IBaseViewModel<...>) {
val refreshState = rememberPagingRefreshState<ItemModel>()
BaseScreenComponent(
viewModel = viewModel,
externalIsRefreshing = refreshState.isRefreshing,
refreshState.refresh() },
) { state, padding ->
val items = state.items?.collectAsLazyPagingItems() // lifecycle-aware
refreshState.bind(items)
PagingVerticalGridComponent(
items = items,
columns = GridCells.Fixed(2),
) { pagingItems ->
items(
count = pagingItems.itemCount,
key = pagingItems.itemKey { it.id }, // Stable keys - required!
) { index ->
pagingItems[index]?.let { ItemCard(it) }
}
}
}
}
Animation Performance
// Use Compose animation APIs
val alpha by animateFloatAsState(
targetValue = if (isVisible) 1f else 0f,
animationSpec = tween(300),
label = "alpha",
)
// Transition API for complex animations
val transition = updateTransition(targetState = isExpanded, label = "expand")
val height by transition.animateDp(label = "height") { expanded ->
if (expanded) 200.dp else 56.dp
}
Debugging Recomposition
Use Android Studio Layout Inspector with "Show Recomposition Counts" enabled.
// Debug-only: recomposition highlighter
@Composable
fun Modifier.recompositionHighlighter(): Modifier {
val count = remember { mutableIntStateOf(0) }
count.intValue++
return this.drawWithContent {
drawContent()
drawRect(
color = Color.Red.copy(alpha = (count.intValue * 0.1f).coerceAtMost(1f)),
size = Size(4.dp.toPx(), size.height),
)
}
}