# Top 20 Jetpack Compose Interview Questions in 2026 > The 20 most-asked Jetpack Compose interview questions: recomposition, state management, navigation, performance, and architecture patterns. - Published: 2026-01-27 - Updated: 2026-03-28 - Author: SharpSkill - Tags: jetpack compose, android, interview, kotlin, ui - Reading time: 14 min --- Jetpack Compose has become the standard UI toolkit for Android development. Technical interviews now routinely test Compose proficiency, from recomposition mechanics to state management and performance optimization. Here are the 20 most frequently asked questions, with detailed answers and code examples. > **How to use this guide** > > Each question includes a structured answer and a code example. Questions are organized by increasing difficulty: fundamentals, intermediate, then advanced. ## Jetpack Compose Fundamentals ### 1. What is the difference between Compose and the XML view system? Compose uses a **declarative** paradigm: the UI is described as a function of state, and the framework handles updates automatically. The traditional XML system is **imperative** — views must be manually manipulated via `findViewById` or View Binding. ```kotlin // DeclarativeExample.kt // Compose: UI updates automatically when count changes @Composable fun Counter() { var count by remember { mutableStateOf(0) } // Reactive state Button(onClick = { count++ }) { // UI declaration Text("Clicks: $count") // Recomposed automatically } } ``` With Compose, there is no need to find a `TextView` reference and update it manually — recomposition handles everything. ### 2. What is recomposition? Recomposition is the process by which Compose re-invokes `@Composable` functions when their state changes. Only functions whose parameters have changed are re-executed, which optimizes performance. ```kotlin // RecompositionExample.kt @Composable fun UserCard(name: String, age: Int) { Column { Text("Name: $name") // Recomposed only if name changes Text("Age: $age") // Recomposed only if age changes StaticBadge() // Not recomposed if its inputs remain the same } } @Composable fun StaticBadge() { Text("Static badge") // Compose knows this function is stable } ``` Key point for interviews: recomposition is **optimistic** (Compose assumes it can be cancelled) and **unordered** (execution order of composables is not guaranteed). ### 3. What does `remember` do? `remember` preserves a value across recompositions. Without `remember`, every recomposition would reset the variable to its initial value. ```kotlin // RememberExample.kt @Composable fun InputField() { // ✅ Value survives recompositions var text by remember { mutableStateOf("") } // ❌ Without remember, text resets to "" on every recomposition // var text by mutableStateOf("") TextField( value = text, onValueChange = { text = it }, // Triggers recomposition label = { Text("Enter text") } ) } ``` ### 4. What is the difference between `remember` and `rememberSaveable`? `remember` preserves values across recompositions but loses them on configuration changes (screen rotation). `rememberSaveable` persists values through configuration changes using the `SavedInstanceState` mechanism. ```kotlin // RememberSaveableExample.kt @Composable fun SearchBar() { // Lost after screen rotation var query by remember { mutableStateOf("") } // Preserved after screen rotation var savedQuery by rememberSaveable { mutableStateOf("") } TextField( value = savedQuery, onValueChange = { savedQuery = it }, placeholder = { Text("Search...") } ) } ``` ## State Management in Compose ### 5. What is state hoisting? State hoisting means moving state up from a composable to its parent. The child composable becomes **stateless**: it receives state as parameters and notifies changes via callbacks. ```kotlin // StateHoistingExample.kt // ✅ Stateless composable — easy to test and reuse @Composable fun EmailInput( email: String, // State provided by parent onEmailChange: (String) -> Unit, // Callback to parent modifier: Modifier = Modifier ) { TextField( value = email, onValueChange = onEmailChange, label = { Text("Email") }, modifier = modifier ) } // Parent manages the state @Composable fun LoginForm() { var email by remember { mutableStateOf("") } EmailInput( email = email, onEmailChange = { email = it } // Parent controls state ) } ``` This pattern is fundamental in Compose and comes up frequently in interviews. ### 6. How does `derivedStateOf` work? `derivedStateOf` creates a derived state that only triggers recomposition when the computation result changes, not on every modification of the source. ```kotlin // DerivedStateExample.kt @Composable fun FilteredList(items: List) { var searchQuery by remember { mutableStateOf("") } // Recalculated only when the filtered result actually changes val filteredItems by remember(items) { derivedStateOf { items.filter { it.contains(searchQuery, ignoreCase = true) } } } Column { TextField(value = searchQuery, onValueChange = { searchQuery = it }) LazyColumn { items(filteredItems) { item -> Text(item) } } } } ``` > **When to use derivedStateOf** > > This mechanism is useful when a state changes frequently but the derived result changes rarely (e.g., a filtered list, a button enabled/disabled based on form validity). ### 7. What is the difference between `StateFlow` and Compose `State`? `StateFlow` (Kotlin coroutines) is a reactive stream from the ViewModel. `State` is Compose's native mechanism for triggering recomposition. In practice, `StateFlow` is collected in a composable via `collectAsStateWithLifecycle()`. ```kotlin // StateFlowExample.kt class UserViewModel : ViewModel() { private val _uiState = MutableStateFlow(UserUiState()) val uiState: StateFlow = _uiState.asStateFlow() } @Composable fun UserScreen(viewModel: UserViewModel = viewModel()) { // Converts StateFlow to State for Compose val uiState by viewModel.uiState.collectAsStateWithLifecycle() Text("Hello, ${uiState.userName}") } ``` The recommendation is to use `collectAsStateWithLifecycle()` (rather than `collectAsState()`) because it respects the lifecycle and stops collection when the screen is no longer visible. ## Side Effects and Lifecycle ### 8. What are the main side effects in Compose? Side effects allow running non-composable code (network calls, logging, navigation) in a controlled manner. The three main ones: ```kotlin // SideEffectsExample.kt @Composable fun AnalyticsScreen(screenName: String) { // LaunchedEffect: runs once when screenName changes LaunchedEffect(screenName) { analyticsTracker.logScreenView(screenName) // Suspended call } // DisposableEffect: with cleanup (like useEffect with cleanup) DisposableEffect(Unit) { val listener = onScrollListener() scrollView.addListener(listener) onDispose { scrollView.removeListener(listener) // Cleanup guaranteed } } // SideEffect: runs after every successful recomposition SideEffect { logger.log("Screen recomposed") // Non-suspended code } } ``` ### 9. When to use `LaunchedEffect` vs `rememberCoroutineScope`? `LaunchedEffect` is tied to the composition: the coroutine is cancelled when the composable leaves the composition or when the key changes. `rememberCoroutineScope` provides a user-controlled scope, useful for user-triggered actions (button clicks). ```kotlin // CoroutineScopeExample.kt @Composable fun DataScreen(userId: String) { // ✅ LaunchedEffect: automatic loading tied to lifecycle LaunchedEffect(userId) { loadUserData(userId) // Re-launched if userId changes } // ✅ rememberCoroutineScope: one-off user action val scope = rememberCoroutineScope() Button(onClick = { scope.launch { refreshData() } // Triggered manually }) { Text("Refresh") } } ``` ## Layouts and Advanced Components ### 10. How does `LazyColumn` work and how does it differ from `RecyclerView`? `LazyColumn` is Compose's equivalent of `RecyclerView`. It only composes elements visible on screen and recycles composables that scroll out of the visible window. ```kotlin // LazyColumnExample.kt @Composable fun UserList(users: List) { LazyColumn( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) // Spacing between items ) { items( items = users, key = { it.id } // Stable key to optimize recompositions ) { user -> UserCard(user) } } } ``` Important point: always provide a stable `key` parameter to avoid unnecessary recompositions when sorting or removing elements. ### 11. How to create a custom layout? Compose allows creating custom layouts via the `Layout` function. This replaces custom `ViewGroup` implementations from the view system. ```kotlin // CustomLayoutExample.kt @Composable fun OverlappingRow( overlapOffset: Dp = (-16).dp, // Negative offset for overlap content: @Composable () -> Unit ) { Layout(content = content) { measurables, constraints -> val placeables = measurables.map { it.measure(constraints) } val width = placeables.sumOf { it.width } + (overlapOffset.roundToPx() * (placeables.size - 1)) val height = placeables.maxOf { it.height } layout(width, height) { var xOffset = 0 placeables.forEach { placeable -> placeable.placeRelative(xOffset, 0) xOffset += placeable.width + overlapOffset.roundToPx() } } } } ``` ### 12. How to implement custom theming with `MaterialTheme`? Theming in Compose relies on `CompositionLocal`. `MaterialTheme` provides color, typography, and shape values accessible throughout the composable tree. ```kotlin // CustomThemeExample.kt // Custom color definitions private val DarkColorScheme = darkColorScheme( primary = Color(0xFF6200EE), secondary = Color(0xFF03DAC6), background = Color(0xFF121212) ) @Composable fun AppTheme(content: @Composable () -> Unit) { MaterialTheme( colorScheme = DarkColorScheme, typography = AppTypography, // Custom typography content = content ) } // Usage in a composable @Composable fun ThemedCard() { Card(colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface // Theme access )) { Text( text = "Content", style = MaterialTheme.typography.bodyLarge // Theme typography ) } } ``` ## Navigation in Compose ### 13. How does Compose Navigation work? Compose Navigation uses a `NavHost` with routes declared as strings (or serializable types since Navigation 2.8+). ```kotlin // NavigationExample.kt @Composable fun AppNavigation() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "home") { composable("home") { HomeScreen(onNavigateToDetail = { id -> navController.navigate("detail/$id") // Navigation with argument }) } composable( route = "detail/{userId}", arguments = listOf(navArgument("userId") { type = NavType.StringType }) ) { backStackEntry -> val userId = backStackEntry.arguments?.getString("userId") ?: "" DetailScreen(userId = userId) } } } ``` ### 14. How to pass data between screens? Simple arguments (String, Int) pass directly via the route. For complex objects, the current recommendation is to use a shared `ViewModel` or pass only an identifier and load data in the destination screen. ```kotlin // NavigationArgsExample.kt // Type-safe navigation with Kotlin Serialization (Navigation 2.8+) @Serializable data class ProfileRoute(val userId: String, val tab: String = "info") // Declaration composable { backStackEntry -> val route = backStackEntry.toRoute() ProfileScreen(userId = route.userId, tab = route.tab) } // Navigation navController.navigate(ProfileRoute(userId = "123", tab = "stats")) ``` > **Anti-pattern to avoid** > > Never pass complex serialized objects in the route. Pass an ID and let the destination screen load the data via the ViewModel. ## Performance and Optimization ### 15. How to prevent unnecessary recompositions? Three main strategies to minimize unnecessary recompositions: ```kotlin // PerformanceExample.kt // 1. Use stable classes (data class with immutable properties) @Stable // Tells Compose this class is stable data class UserState( val name: String, val avatar: String ) // 2. Extract lambdas with remember @Composable fun OptimizedList(onItemClick: (String) -> Unit) { val stableCallback = remember(onItemClick) { onItemClick } LazyColumn { items(100) { index -> ItemRow(onClick = { stableCallback("item_$index") }) } } } // 3. Use key() to help Compose identify elements @Composable fun UserTabs(users: List) { Column { users.forEach { user -> key(user.id) { // Stable identity UserRow(user) } } } } ``` ### 16. How to profile Compose app performance? Android Studio's Layout Inspector displays recomposition counts per composable. The `debugInspectorInfo` flag and `CompositionTracer` help with diagnostics. ```kotlin // ProfilingExample.kt // Enable recomposition counters in debug @Composable fun DebugRecomposition(tag: String, content: @Composable () -> Unit) { val recompositionCount = remember { mutableIntStateOf(0) } SideEffect { recompositionCount.intValue++ // Incremented on every recomposition Log.d("Recomposition", "$tag: ${recompositionCount.intValue} times") } content() } // Usage DebugRecomposition("UserCard") { UserCard(user) } ``` Additionally, **Compose Compiler Metrics** generates a detailed report of skippable, restartable functions and stable/unstable classes. ### 17. What is the `Modifier` and why is it important? `Modifier` is an ordered chain of instructions that modifies the appearance and behavior of a composable. The order of modifiers directly impacts rendering. ```kotlin // ModifierOrderExample.kt @Composable fun ModifierOrderDemo() { // ❌ Padding THEN background = padding not colored Text( text = "Hello", modifier = Modifier .padding(16.dp) .background(Color.Red) ) // ✅ Background THEN padding = padding is colored Text( text = "Hello", modifier = Modifier .background(Color.Red) .padding(16.dp) ) } ``` Best practice: always accept a `modifier: Modifier = Modifier` parameter in reusable composables to allow customization by the parent. ## Architecture and Advanced Patterns ### 18. How to structure a Compose screen with a ViewModel? The recommended pattern separates UI state in a `data class`, events in a `sealed interface`, and the ViewModel handles business logic. ```kotlin // ScreenArchitectureExample.kt // UI State data class ProfileUiState( val user: User? = null, val isLoading: Boolean = false, val error: String? = null ) // User events sealed interface ProfileEvent { data object Refresh : ProfileEvent data class UpdateName(val name: String) : ProfileEvent } // ViewModel class ProfileViewModel(private val repo: UserRepository) : ViewModel() { private val _uiState = MutableStateFlow(ProfileUiState(isLoading = true)) val uiState = _uiState.asStateFlow() fun onEvent(event: ProfileEvent) { when (event) { is ProfileEvent.Refresh -> loadProfile() is ProfileEvent.UpdateName -> updateName(event.name) } } } // Compose screen @Composable fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() ProfileContent( uiState = uiState, onEvent = viewModel::onEvent // Event delegation ) } ``` ### 19. How to test composables? Compose provides a testing library with `ComposeTestRule` for UI tests and semantic assertions. ```kotlin // ComposableTestExample.kt @get:Rule val composeTestRule = createComposeRule() @Test fun counter_incrementsOnClick() { composeTestRule.setContent { Counter() // The composable under test } // Verify initial state composeTestRule.onNodeWithText("Clicks: 0").assertIsDisplayed() // Simulate a click composeTestRule.onNodeWithText("Clicks: 0").performClick() // Verify new state composeTestRule.onNodeWithText("Clicks: 1").assertIsDisplayed() } ``` For unit testing stateless composables, testing the ViewModel separately with standard JUnit/Turbine tests is often more efficient. ### 20. How to integrate Compose into an existing XML-based app? Interoperability is bidirectional: `ComposeView` embeds Compose in XML, and `AndroidView` uses classic views within Compose. ```kotlin // InteropExample.kt // Compose in XML (in a Fragment or Activity) class ProfileFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed ) setContent { AppTheme { ProfileScreen() } } } } } // XML View in Compose @Composable fun LegacyMapView() { AndroidView( factory = { context -> MapView(context).apply { onCreate(null) } }, update = { mapView -> mapView.getMapAsync { /* config */ } } ) } ``` > **Recommended migration strategy** > > Migrate screen by screen, starting with the simplest screens. Every new screen should be entirely in Compose, while existing screens migrate progressively. ## Conclusion These 20 questions cover the fundamentals every Android developer needs to master for a Jetpack Compose interview. Here is a recap checklist: - ✅ Understand recomposition and its optimistic behavior - ✅ Master `remember`, `rememberSaveable`, and `derivedStateOf` - ✅ Apply state hoisting systematically - ✅ Know the side effects (`LaunchedEffect`, `DisposableEffect`, `SideEffect`) - ✅ Optimize performance (stable classes, keys, lambdas) - ✅ Structure screens with ViewModel + UiState + Events - ✅ Test composables with `ComposeTestRule` - ✅ Handle Compose/Views interoperability --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/android/jetpack-compose-interview-questions