Jetpack Compose
1. Recomposition
Q: What is recomposition in Jetpack Compose?
Recomposition is when Compose re-executes composable functions to update the UI after state changes. Compose intelligently recomposes only the composables that read the changed state, not the entire tree.
Q: What triggers recomposition?
Any state read by a composable changing its value. This includes mutableStateOf, StateFlow.collectAsState(), or any State<T> object that the composable reads during execution.
Q: What are the key rules composables must follow?
- Idempotent: Same inputs produce same UI
- Side-effect free: No external mutations in the function body
- Independent of order: Don't assume execution order
- Can be skipped: Compose may skip recomposition if inputs haven't changed
Q: How does Compose decide what to skip?
Compose compares parameters using equals(). If all parameters are equal to the previous composition, it skips recomposition. For this to work, parameters must be stable—either primitives, immutable data classes, or annotated with @Stable/@Immutable.
2. Stability and Performance
Q: What makes a type "stable" in Compose?
A stable type guarantees that:
equals()always returns the same result for the same instances- If a public property changes, Compose is notified
- All public properties are also stable
Primitives, String, and immutable data classes with stable properties are stable by default.
Q: What makes a type "unstable"?
- Mutable collections (
MutableList,ArrayList) - Data classes with mutable properties
- Classes from external libraries Compose can't verify
- Lambdas captured in composition
Q: How do you fix stability issues?
| Problem | Solution |
|---|---|
| Mutable collection | Use immutable List, Set, Map |
| External library class | Wrap in stable wrapper or use @Immutable |
| Lambda recreation | Hoist lambda or use remember |
| Unknown class | Annotate with @Stable if you guarantee stability |
Q: How do you debug recomposition issues?
- Use Layout Inspector's "Show Recomposition Counts"
- Add
SideEffect { log("Recomposed") }temporarily - Check stability with Compose Compiler reports
- Profile with Android Studio Profiler
3. State Management
Q: Explain the different ways to hold state in Compose.
| API | Survives Recomposition | Survives Config Change | Survives Process Death |
|---|---|---|---|
remember |
✅ | ❌ | ❌ |
rememberSaveable |
✅ | ✅ | ✅ |
ViewModel |
✅ | ✅ | ❌ (use SavedStateHandle) |
Q: What is state hoisting?
Moving state up from a composable to its caller, making the composable stateless. The composable receives the state value and a callback to request changes. Benefits:
- Reusable composables
- Easier testing
- Single source of truth
- Parent controls the state
Q: When should state live in ViewModel vs Composable?
| ViewModel | Composable (remember) |
|---|---|
| Business logic state | Pure UI state |
| Data from repository | Animation state |
| Survives config change | Scroll position, expansion state |
| Shared across composables | Local to one composable |
4. Side Effects
Q: What are side effects and why do they need special handling?
Side effects are operations that escape the composable's scope—network calls, database writes, navigation, analytics. Composables can recompose at any time, in any order, so side effects in the body would execute unpredictably.
Q: Explain the side effect APIs.
| API | Purpose | Lifecycle |
|---|---|---|
LaunchedEffect(key) |
Launch coroutine | Cancelled when key changes or leaves composition |
DisposableEffect(key) |
Setup/cleanup resources | Cleanup called when key changes or leaves composition |
SideEffect |
Non-suspending effect on every successful recomposition | Runs after every recomposition |
rememberCoroutineScope() |
Get scope for event handlers | Tied to composition lifecycle |
derivedStateOf |
Compute value from other states, avoiding recomposition | Updates only when result changes |
produceState |
Convert non-Compose state to Compose state | Runs coroutine to produce values |
Q: LaunchedEffect vs rememberCoroutineScope?
- LaunchedEffect: For effects that should run when entering composition or when a key changes. Automatic lifecycle management.
- rememberCoroutineScope: For effects triggered by user events (button clicks). You control when to launch.
Q: When do you use DisposableEffect?
For resources that need explicit cleanup:
- Registering/unregistering listeners
- Binding to lifecycle
- Managing third-party SDK lifecycles
- Callback registrations
5. Composition Local
Q: What is CompositionLocal?
A way to pass data implicitly through the composition tree without explicit parameters. Useful for cross-cutting concerns like theme, navigation, or locale.
Q: compositionLocalOf vs staticCompositionLocalOf?
| Type | Recomposition | Use Case |
|---|---|---|
compositionLocalOf |
Only readers recompose when value changes | Frequently changing values |
staticCompositionLocalOf |
Entire subtree recomposes when value changes | Rarely changing values (theme, locale) |
Q: When should you use CompositionLocal?
Sparingly. Good uses:
- Theme/colors (already provided by MaterialTheme)
- Navigation controller
- Dependency injection in Compose
Avoid for business data—use parameters for explicit, traceable data flow.
6. Lists and Performance
Q: How do you build efficient lists in Compose?
Use LazyColumn/LazyRow instead of Column/Row with forEach. Lazy composables only compose visible items.
Q: What is the key parameter and why is it important?
Keys help Compose identify which items changed, moved, or stayed the same. Without keys, Compose uses position—moving an item causes unnecessary recomposition of all items between old and new positions. With stable keys (like IDs), Compose preserves state and animates correctly.
Q: How do you optimize LazyColumn performance?
- Always provide keys: Use unique, stable identifiers
- Avoid heavy computation in items: Use
rememberor move to ViewModel - Use
contentType: Helps Compose reuse compositions - Avoid nesting scrollable containers: Causes measurement issues
- Use
derivedStateOffor scroll-dependent state: Reduces recomposition
7. Navigation
Q: How does Navigation work in Compose?
Navigation Compose uses a NavHost composable with a NavController. Routes are strings (optionally with arguments). Each route maps to a composable destination.
Q: How do you pass arguments between screens?
- Define route with placeholders:
"user/{userId}" - Navigate with values:
navController.navigate("user/123") - Receive in destination via
NavBackStackEntry.arguments
For complex objects, pass IDs and fetch data in the destination's ViewModel.
Q: How do you handle navigation events from ViewModel?
Expose a one-time event (Channel or SharedFlow) that the UI collects and acts upon. Don't hold NavController in ViewModel—it's a UI concern.
8. Interop with Views
Q: How do you use Views inside Compose?
Use AndroidView composable. Provide a factory to create the View and an update lambda for when state changes. The View is created once and updated on recomposition.
Q: How do you use Compose inside Views?
Use ComposeView in XML or create it programmatically. Call setContent {} to provide composables. In Fragments, set viewCompositionStrategy to match the Fragment's lifecycle.
Q: What's important when mixing Compose and Views?
- Theme bridging: Use
MdcThemeor custom bridging - Lifecycle: Ensure Compose respects the View's lifecycle owner
- State: Be careful about state ownership—one source of truth
- Performance: Minimize interop boundaries for best performance
Quick Reference
| Topic | Key Points |
|---|---|
| Recomposition | Triggers on state change; skips stable unchanged inputs; keep composables pure |
| Stability | Immutable = stable; mutable collections = unstable; use @Stable/@Immutable when needed |
| State | remember (composition), rememberSaveable (config change), ViewModel (business logic) |
| Side Effects | LaunchedEffect for coroutines, DisposableEffect for cleanup, SideEffect for every recomposition |
| Lists | Use LazyColumn; always provide keys; use contentType for mixed lists |
| Navigation | NavHost + NavController; string routes; pass IDs not objects |