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 interview questions for Android developers

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.

DeclarativeExample.ktkotlin
// 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.

RecompositionExample.ktkotlin
@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.

RememberExample.ktkotlin
@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.

RememberSaveableExample.ktkotlin
@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.

StateHoistingExample.ktkotlin
// ✅ 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.

DerivedStateExample.ktkotlin
@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) }
        }
    }
}
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<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().

StateFlowExample.ktkotlin
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:

SideEffectsExample.ktkotlin
@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).

CoroutineScopeExample.ktkotlin
@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.

LazyColumnExample.ktkotlin
@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.

CustomLayoutExample.ktkotlin
@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.

CustomThemeExample.ktkotlin
// 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
        )
    }
}

13. How does Compose Navigation work?

Compose Navigation uses a NavHost with routes declared as strings (or serializable types since Navigation 2.8+).

NavigationExample.ktkotlin
@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.

NavigationArgsExample.ktkotlin
// 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"))
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:

PerformanceExample.ktkotlin
// 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.

ProfilingExample.ktkotlin
// 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.

ModifierOrderExample.ktkotlin
@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.

ScreenArchitectureExample.ktkotlin
// 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.

ComposableTestExample.ktkotlin
@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.

InteropExample.ktkotlin
// 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.

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, 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

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#jetpack compose
#android
#interview
#kotlin
#ui

Share

Related articles