Performance & Best Practices
A checklist for keeping an advanced Compose app fast and correct.
Compose performance
- Minimize recomposition scope. Read state as low in the tree as possible so only small composables recompose.
- Use stable types. Prefer immutable
data classes andList(notMutableList) as composable parameters; unstable types force recomposition. - Key your lists. Provide
key = { it.id }inLazyColumnitems. - Hoist
remembercorrectly. Expensive computations go inremember(key) { ... }, not in the composable body. - Avoid backwards writes. Do not write to state you have already read in the same composition.
derivedStateOffor state computed from other state, to avoid extra recompositions.
Threading
- Never block the main thread. Wrap blocking I/O in
withContext(Dispatchers.IO). - Use
viewModelScope/lifecycleScopeso work is cancelled with its owner. - Collect flows with
collectAsStateWithLifecycle()so collection stops when the UI is not visible.
Memory and leaks
- Do not hold
Context/Activity/Viewreferences in aViewModel. - Cancel coroutines and unregister listeners (
DisposableEffect) when scopes end. - Prefer
applicationContextfor long-lived needs.
App size and startup
- Enable R8/minification and resource shrinking for release builds.
- Defer heavy initialization off the startup path.
A pre-release checklist
- No blocking work on the main thread
- Lists use stable keys and items are stable
- State is hoisted; recomposition scopes are small
- ViewModels hold no
View/Context - Coroutines use lifecycle-aware scopes
- Release build minified and tested
- Unit + Compose tests green in CI
The worked example
examples/android/ brings the architecture together: a ViewModel exposing an
immutable StateFlow UI state, a repository behind an interface, navigation, and
JVM unit tests — all building in CI. The Room, Hilt, Retrofit, and WorkManager
chapters above provide drop-in code to extend it.
Exercises
-
Audit a screen for unnecessary recompositions (use the Layout Inspector / recomposition counts).
-
Move a blocking call into
withContext(Dispatchers.IO)and confirm the UI stays responsive.
Previous: Testing · Next: Part 9: Real-World Projects