20 Most Asked Jetpack Compose Interview Questions for Android Developers
1. What is recomposition in Jetpack Compose?
Recomposition is the process where Jetpack Compose re-executes your Composable functions with new data to update the UI hierarchy.Instead of mutating individual views (like in XML), Compose recreates the UI from scratch for the parts that changed. It uses an intelligent compiler to optimise this process, skipping any Composable functions whose inputs (parameters) have not modified.
2. What triggers recomposition?
Recomposition is triggered exclusively by a change in a state object that a Composable function reads.
- State Updates: Modifications to a Compose
State<T>(e.g., a value wrapped inmutableStateOf). - Flow/LiveData Streams: Emitting new values into a
StateFloworLiveDatathat has been converted to Compose state using.collectAsStateWithLifecycle()or.observeAsState().
3. How can you avoid unnecessary recomposition?
You can minimise performance overhead and redundant redraws using these practices:
- Use
remember: Cache expensive computations inside a Composable so they don’t re-run on every frame. - Pass stable parameters: Ensure your data models are immutable or marked with
@Stable/@Immutableannotations so Compose knows it can safely skip them if their references don't change. - Use Lambda modifiers: When reading frequently changing state (like scroll offsets), pass a lambda to the modifier (e.g.,
Modifier.offset { IntOffset(0, scrollState.value) }). This defers state reading to the layout/draw phase, bypassing the recomposition phase entirely. - Use
derivedStateOf: Buffer high-frequency state updates (explained in Question 7).
4. What is the difference between
remember and rememberSaveable?remember: Caches state across recompositions. However, the state is completely lost if the Composable leaves the composition completely or during configuration changes (like screen rotation or changing to dark mode).rememberSaveable: Caches state across recompositions and survives configuration changes or process death. It automatically saves values into an AndroidBundle. It only works for data types that can be put in a Bundle (or custom types using a customSaver).
5. What is state hoisting and why is it important?
State hoisting is an architectural pattern where you move UI state up to a Composable’s caller to make the lower Composable stateless. This is achieved by replacing state variables with two parameters: the current value and an event lambda to request changes (e.g.,value: String, onValueChange: (String) -> Unit).Why it is important:
- Single Source of Truth: Prevents state duplication and sync bugs.
- Reusability: Stateless components can be rendered anywhere with different data.
- Testability: You can easily test a stateless Composable by passing mock data and verifying lambda callbacks.
6. What is the difference between
mutableStateOf and StateFlow?mutableStateOf: A state container built strictly for the Compose runtime. It is lifecycle-aware by default within the composition tree. When its value changes, it directly schedules a recomposition for any Composable reading it.StateFlow: A state container from Kotlin Coroutines / architecture layers. It is framework-agnostic (can be used in pure Kotlin, ViewModels, or XML). To use it in Compose, you must explicitly convert it to a Compose state using.collectAsStateWithLifecycle().
7. What is
derivedStateOf and where should you use it?derivedStateOfcreates a Compose state that depends on other state objects, acting as a calculation buffer. It guarantees that the calculation executes only when the dependencies change, and it only triggers recomposition if the final output of the calculation changes.Where to use it: Use it when a state changes rapidly (high-frequency), but your UI only cares about a condition derived from it.
- Example: A scroll position updates every single pixel. If you want a "Scroll to Top" button to appear only when
scrollPosition > 100, wrapping that condition inderivedStateOfstops the button from constantly recomposing on every pixel scrolled.
8. What is the difference between
LaunchedEffect and DisposableEffect?LaunchedEffect: Used for asynchronous side-effects. It launches a coroutine block when it enters the composition. The coroutine cancels automatically if the key changes or if the Composable leaves the screen.DisposableEffect: Used for synchronous side-effects that require cleanup. It does not offer a coroutine scope. It enforces anonDispose {}block at the end to clean up resources (like unregistering a broadcast receiver, closing a listener, or stopping a sensor) when the key changes or the Composable leaves the screen.
9. What is a side-effect and when is it useful?
A side-effect is anything that happens outside the control or scope of a Composable function, changing the state of the app outside the UI. Because Composables can execute unpredictably, side-effects must be managed safely inside Compose Effect APIs.When it is useful:
- Fetching data from a network on screen load (
LaunchedEffect). - Displaying a
Snackbaror navigation actions based on a UI state. - Initializing and tearing down third-party SDK listeners (
DisposableEffect).
10. What is
CompositionLocal and how does it work?CompositionLocalis a tool for passing data down through the Composable tree implicitly, without explicitly defining it as a parameter in every single Composable function (avoiding "prop-drilling").How it works: You create a key provider (usingcompositionLocalOforstaticCompositionLocalOf). You then wrap a parent UI tree inside aCompositionLocalProvider(LocalYourData provides data) { ... }. Any child or deeply nested grandchild Composable inside that tree can access the data by callingLocalYourData.current. Jetpack Compose uses this internally to pass down global utilities likeLocalContext,LocalTheme, andLocalSoftwareKeyboardController.
11. What is the difference between Column and LazyColumn?
Column(Eager Loading): Measures and renders all its child items simultaneously at layout time, regardless of whether they are visible on the screen.- When to use: Use it for small, static layouts with a fixed, predictable number of items (e.g., a settings form, a profile layout).
LazyColumn(Lazy Loading): The Compose equivalent of aRecyclerView. It renders and recomposes items only as they scroll into view and recycles their layout structures when they scroll off-screen.- When to use: Use it for large, infinite, or dynamically changing lists (e.g., a social media feed, product grids) to prevent memory overhead and UI stuttering.
12. How should you use a key in LazyColumn?
By default, items in aLazyColumnare tracked by their index position. If you add, delete, or reorder items in the list, Compose will misidentify which item moved, causing unnecessary recompositions or resetting the state of internal UI components.Providing a stable and unique identifier using thekeyparameter fixes this issue.Code Example :
@Composable
fun ProductList(products: List<Product>) {
LazyColumn {
LazyColumn {
// Pass a unique key (like a database ID) to each item
items(
items = products,
key = { product -> product.id }
) {
) { product ->
ProductRow(product)
}
}
}
ProductRow(product)
}
}Interview Tip: Always avoid using the item's index position orRandom.nextInt()as a key. A valid key must be stable across recompositions and layout passes.
13. What is SnapshotFlow?
snapshotFlow transforms Compose State<T> objects into a standard Kotlin Coroutine Flow. It monitors any Compose state objects read inside its trailing lambda block and emits a new value into the downstream coroutine flow whenever that state changes. It acts as the reverse bridge of
.collectAsState().Code Example
kotlin
@Composable
fun InfiniteScrollDetector(lazyListState: LazyListState, onLoadMore: () -> Unit) {
LaunchedEffect(lazyListState) {
snapshotFlow { lazyListState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index ->
if (index > 20) {
onLoadMore() // Safely trigger an asynchronous callback outside Compose
}
}
}
}14. How do you collect a Flow in Compose?
You should never collect a generic KotlinFlowdirectly inside a Composable function because it breaks structured concurrency and causes memory leaks. Instead, you must convert the Flow into a ComposeState<T>so that updates automatically trigger recomposition.There are two primary extension functions to achieve this conversion:Code Example@Composablefun FlowCollectionScreen(viewModel: HomeViewModel) {// Correct way: Collects safely based on UI lifecycleval uiState by viewModel.uiState.collectAsStateWithLifecycle()// Legacy way (Not recommended for Android UI development)// val oldUiState by viewModel.uiState.collectAsState()Text(text = uiState.username)
}
15. What is
collectAsStateWithLifecycle and why is it preferred?collectAsStateWithLifecycle()is a lifecycle-aware state collector specifically designed for Android applications (available via theandroidx.lifecycle.lifecycle-runtime-composelibrary).Why it is preferred overcollectAsState():
collectAsState()keeps collecting flow emissions actively even if the app goes to the background, wasting CPU and memory resources.collectAsStateWithLifecycle()automatically drops the underlying coroutine collection when the hostActivityorFragmentdrops below a specific lifecycle state (by default,Lifecycle.State.STARTED). When the app moves back to the foreground, it seamlessly resubscribes to the stream.
16. How does navigation work in Jetpack Compose?
Navigation in Compose uses the Navigation Component with Type-Safe APIs (introduced in Navigation 2.8+). It replaces string-based route URLs with concrete Kotlin objects or data classes.
Code Example
kotlin
import kotlinx.serialization.Serializable
// Define destinations as type-safe objects/classes
@Serializable object HomeRoute
@Serializable data class DetailsRoute(val itemId: String)
@Composable
fun MainNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = HomeRoute) {
composable<HomeRoute> {
HomeScreen(onItemClick = { id ->
navController.navigate(DetailsRoute(itemId = id))
})
}
composable<DetailsRoute> { backStackEntry ->
// Extract typed argument automatically
val details: DetailsRoute = backStackEntry.toRoute()
DetailsScreen(itemId = details.itemId)
}
}
}
17. How do you pass arguments between Compose screens?
Using the type-safe Navigation library, you pass arguments by embedding parameters inside the@Serializabledestination class. The library handles the serialization to and from the back stack entry under the hood.
Step 1: Define the target route class with custom arguments
kotlin
@Serializable
data class ProfileRoute(val userId: Int, val isPremium: Boolean)
Step 2: Navigate and pass data
kotlin
navController.navigate(ProfileRoute(userId = 9921, isPremium = true))
Step 3: Extract arguments inside the Destination composable
kotlin
composable<ProfileRoute> { backStackEntry ->
val profile: ProfileRoute = backStackEntry.toRoute()
ProfileScreen(userId = profile.userId, isPremium = profile.isPremium)
}
18. How do you handle configuration changes in Compose?
Unlike the legacy XML view system, Compose does not rely on recreating the whole view layout from a configuration folder (like layout-land). Instead:
- Layout Adapts Electronically: The UI relies on layout bounds (e.g.,
BoxWithConstraints) to change design automatically when size properties flex. - State Retention: Local Composable states are retained across rotations using
rememberSaveable(saves state to a Bundle) or inside architecturalViewModels(survives rotation entirely).
Code Example
kotlin
@Composable
fun AdaptiveScreen() {
// Survives screen rotation safely
var textInput by rememberSaveable { mutableStateOf("") }
BoxWithConstraints {
if (maxWidth < 600.dp) {
PortraitLayout(textInput, onValueChange = { textInput = it })
} else {
LandscapeLayout(textInput, onValueChange = { textInput = it })
}
}
}
19. What are Stable and Immutable objects in Compose?Compose utilizes these markers to identify types that are guaranteed not to change unexpectedly, allowing the runtime compiler to skip recomposition for optimized performance.
@Immutable: Applied to classes where all properties are completely immutable (val) and can never modify after construction (e.g., a standard data class).@Stable: Applied to classes whose properties can mutate, but the Compose runtime will be explicitly notified via standard observable states when mutations occur.
Code Example
kotlin
@Immutable
data class UserProfile(
val name: String,
val email: String
) // Compose safely skips UI blocks rendering this object if references match
@Stable
class RealtimeClockState {
var currentTimeMillis by mutableStateOf(0L)
} // Modifies internally, but informs Compose cleanly when changes happen
Crucial Interview Insight: Standard Kotlin Collections (List,Map) are inferred as Unstable by the Compose compiler because underlying implementations (likeArrayList) are mutable. Wrap lists using Kotlinx Immutable Collections (ImmutableList) or annotate your wrapper models with@Stable.
20. How do you optimize Compose UI performance in a large app?
- Deconstruct with Lambdas (Defer State Reads):Pass high-frequency states inside lambda blocks directly to modifiers rather than raw values to avoid full recomposition loops.kotlin
// Bad: Recomposes full view scope on every pixel scrolled Modifier.offset(y = scrollState.value.dp) // Optimized: Bypasses composition phase; executes straight in Layout/Draw phase Modifier.offset { IntOffset(0, scrollState.value) } - Explicit Keying: Use unique keys in all
LazyColumn/LazyRowitems to optimize positional calculations.[1] - Encapsulate inside
derivedStateOf: Buffer high-frequency input streams into discrete state updates before letting them hit visual components. - Enforce Multi-Module Stability: Add a
compose_compiler.cfgconfiguration file to enforce type stability across separate code modules in large architectures. - Profile via Layout Inspector: Utilize Android Studio's Layout Inspector regularly to track recomposition counters and isolate performance bottlenecks. [1, 2]
Comments
Post a Comment