Top 20 Jetpack Compose Interview Questions in 2026
The 20 most-asked Jetpack Compose interview questions: recomposition, state management, navigation, performance, and architecture patterns.

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.
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.
// 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.
@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.
@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.
@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.
// ✅ 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.
@Composable
fun FilteredList(items: List<String>) {
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) }
}
}
}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<T>?
StateFlow (Kotlin coroutines) is a reactive stream from the ViewModel. State<T> is Compose's native mechanism for triggering recomposition. In practice, StateFlow is collected in a composable via collectAsStateWithLifecycle().
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
}
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
// Converts StateFlow to State<T> 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:
@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).
@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")
}
}Ready to ace your Android interviews?
Practice with our interactive simulators, flashcards, and technical tests.
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.
@Composable
fun UserList(users: List<User>) {
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.
@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.
// 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+).
@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.
// Type-safe navigation with Kotlin Serialization (Navigation 2.8+)
@Serializable
data class ProfileRoute(val userId: String, val tab: String = "info")
// Declaration
composable<ProfileRoute> { backStackEntry ->
val route = backStackEntry.toRoute<ProfileRoute>()
ProfileScreen(userId = route.userId, tab = route.tab)
}
// Navigation
navController.navigate(ProfileRoute(userId = "123", tab = "stats"))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:
// 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<User>) {
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.
// 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.
@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.
// 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.
@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.
// 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 */ } }
)
}Migrate screen by screen, starting with the simplest screens. Every new screen should be entirely in Compose, while existing screens migrate progressively.
Ready to ace your Android interviews?
Practice with our interactive simulators, flashcards, and technical tests.
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, andderivedStateOf - ✅ 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
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

Jetpack Compose: Advanced Animations Step by Step
Complete guide to advanced Compose animations: transitions, AnimatedVisibility, Animatable, gestures and performance for smooth Android interfaces.

Kotlin 2.3 for Android: Name-Based Destructuring, KMP and Interview Questions 2026
Kotlin 2.3 interview questions covering name-based destructuring, Kotlin Multiplatform, context parameters, coroutines and Flow. Prepare for Android developer interviews in 2026 with real-world code examples.

MVVM vs MVI: Which Architecture to Choose in 2026?
In-depth comparison of MVVM and MVI on Android: pros, cons, use cases, and a practical guide to choosing the right architecture in 2026.