On this page
Jetpack Compose: Practical Notes & Pitfalls to Watch For
Author : ChungHA (RxMobileTeam)
Jetpack Compose: Practical Notes & Pitfalls to Watch For
1. What is recomposition? When does it happen? How do you detect and optimize unnecessary recomposition?
- Recomposition is the process where Jetpack Compose redraws (rebuilds) part or all of the UI tree when the data (state) a composable depends on changes.
- It happens when:
- A state source used by the composable (e.g.,
mutableStateOf,LiveData,StateFlow…) changes its value. - The parameters passed into the composable change.
- A state source used by the composable (e.g.,
- Detecting it:
- Use logs or debug tools:
Log.d("Recomp", "Composable X recomposed!") - Use Layout Inspector (Android Studio) to track the recomposition count.
- Use logs or debug tools:
- Optimizing it:
- Split composables into smaller pieces and pass only the props they actually need.
- Use
remember, immutable objects/classes, and annotate with@Stablewhen appropriate. - Avoid passing lambdas/objects that are created on the fly inside the composable body.
- Avoid creating new lists/collections on every recomposition.
2. How to use remember, rememberSaveable, and derivedStateOf for different use cases?
remember:- Stores data / memoized results short-term (within the lifecycle of the current composable tree).
- Examples: caching an object, a callback, UI state that only needs to be remembered within a small session.
rememberSaveable:- Persists data across process death and configuration changes.
- Examples:
TextFieldvalue, form state, scroll position.
derivedStateOf:- Computes data derived from multiple states and only updates when its input states change.
- Examples: filtered list, aggregates/totals, a view mode computed from several smaller variables.
3. When should you use key in LazyColumn/LazyList? What are the risks if you don’t?
- Use
keywhen:- Displaying a dynamic list (items can be added, removed, or moved).
- Items can change position or content but retain identity.
- Risks without a key:
- Compose can’t tell which item is old/new → may reuse views incorrectly, cause UI flicker, or lose transient state (input, scroll position…).
- Performance drops due to excessive redraws.
- Usage:
items(userList, key = { it.id }) { user ->
// ...
}4. How does Compose handle the Slot API and CompositionLocal? Real-world cases for custom CompositionLocals.
- Slot API:
- Compose lets you pass UI blocks into a parent component via lambdas (slots). E.g., custom header, custom button content.
- Example:
@Composable
fun MyCard(content: @Composable () -> Unit) {
// ...
}- CompositionLocal:
- Allows passing context or data deep down the tree without threading it through every prop.
- Use cases: theme, locale, spacing, user/session, permission state…
- Example:
val LocalSpacing = compositionLocalOf { 8.dp }
CompositionLocalProvider(LocalSpacing provides 16.dp) {
// children can read LocalSpacing.current
}5. How to build a custom layout composable? How to optimize heavy layouts in Compose?
- Custom layout:
- Use the
Layoutfunction, or aModifier.layoutto build custom layout logic. - Example:
- Use the
@Composable
fun MyCustomLayout(content: @Composable () -> Unit) {
Layout(content = content) { measurables, constraints ->
// Measure children and place them based on custom rules
// return layout(width, height) { placeables.forEach { it.place(x, y) } }
}
}- Optimizing heavy layouts:
- Avoid deep, nested layout hierarchies.
- Prefer standard composables (
Row,Column,Box) or well-designed custom layouts. - Use
Modifier.layoutIdwithLazyLayoutwhere applicable. - Reuse layout logic; don’t explode into too many tiny composables if it doesn’t help.
6. Distinguish @Composable, @Stable, @Immutable, @ReadOnlyComposable. Impact on performance?
@Composable: Marks a function that can participate in the compose tree and is controlled by the Compose runtime.@Stable: Guarantees an object’s observable properties don’t change unexpectedly; helps Compose decide when it can skip recomposition.@Immutable: All properties arevaland immutable; Compose can safely skip recomposition when the reference is unchanged.@ReadOnlyComposable: For read-only, side-effect-free functions; allows calls from any thread and runtime optimizations.- Performance impact:
- Correctly applying
@Stable/@Immutableenables Compose to skip recompositions and improve performance. @Composableis required so Compose can understand and manage the UI function.
- Correctly applying
7. Passing mutable objects (lists, classes) into a composable—what to watch for? Optimal solutions?
- Caveats:
- If a mutable object changes without creating a new instance, Compose may not detect it to rebuild the UI.
- Creating a brand-new object on every recomposition can hurt performance.
- Solutions:
- Prefer immutable objects /
data classand immutable lists. - If you must pass a mutable object, ensure you create a new instance when changes occur (e.g.,
copy). - Use
@Stableor@Immutableto communicate object characteristics to Compose.
- Prefer immutable objects /
8. Recomposition, skipping, and invalidation in Compose. When does Compose auto-skip?
- Invalidation: When a state or prop a composable depends on changes, Compose marks that region as invalid.
- Recomposition: Compose calls the invalid composables again to update the UI tree.
- Skipping: If Compose determines the inputs haven’t changed (via
equals/hashCode/@Stable/@Immutable), it can skip recomposition for that composable. - Auto-skip occurs when:
- Parameters and state values are unchanged (new value equals old value; immutable objects).
9. When can Compose cause memory leaks? How to detect and prevent them?
- Leaks can occur when:
- Holding references to
Context/Activity/View/Lifecycleoutside the composable’s proper scope. - Registering listeners/callbacks without proper teardown (e.g., not removing listeners in
DisposableEffect).
- Holding references to
- Detection:
- Use Profiler, LeakCanary, or add debug logs in
DisposableEffect/LaunchedEffect.
- Use Profiler, LeakCanary, or add debug logs in
- Prevention:
- Keep references alive only within suitable scopes (don’t hold onto
Contextlong-term). - Use
DisposableEffectto clean up resources. - Prefer
rememberand avoid passingContextinto globals or long-lived lambdas.
- Keep references alive only within suitable scopes (don’t hold onto
10. Performance comparison: Compose vs classic Views for very large lists (10k, 100k items). How to measure and optimize?
- Comparison:
- Compose with
LazyColumnuses lazy loading similar toRecyclerView. If you don’t use keys correctly or structure code poorly, it’s easy to trigger excessive recomposition. - Classic Views (
RecyclerView) are highly optimized for large lists via the ViewHolder pattern.
- Compose with
- Measuring:
- Use Layout Inspector, Profiler in Android Studio, recomposition logs/counters, and track GC.
- Measure FPS and memory usage while scrolling large lists.
- Optimizing:
- Always provide a stable
keytoLazyColumn. - Avoid rebuilding item composables; factor items into smaller composables where it helps.
- Minimize heavy logic/computation inside item composables.
- Use Paging for very large datasets.
- Always provide a stable