← Back to list

Jetpack Compose Interview Questions in 2026

1. What is the difference between Compose and the XML view system?

Abhishek Srivastava · 2026-07-21 11:17 · 1 claps · 22.1 min read paywalled
#jetpack-compose #jetpack-compose-tutorial #android-jetpack #android #android-app-development
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

Jetpack Compose Interview Questions in 2026

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. Explain the Compose compilation process and how @Composable functions work.

The Compose compiler transforms @Composable functions into state machines that can efficiently update the UI.

Compilation Process:

  1. Compose Compiler Plugin analyzes @Composable functions
  2. Transforms functions to track state and dependencies
  3. Generates code for recomposition and skipping
  4. Creates slot table for efficient UI tree management
// What you write
@Composable
fun Greeting(name: String) {
    Text("Hello $name")
}

// What the compiler generates (simplified)
fun Greeting(name: String, composer: Composer, changed: Int) {
    composer.startRestartGroup()

    if (changed and 0b1 == 0) {
        // Skip recomposition if name hasn't changed
        composer.skipToGroupEnd()
        return
    }

    Text("Hello $name", composer, 0)
    composer.endRestartGroup()
}

3. 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.

When Recomposition Occurs:

  • State objects change (mutableStateOf)
  • Parameters passed to composable change
  • External state sources notify changes
@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).

4. 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") }
    )
}

5. 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

6. 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.

7. 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) }
        }
    }
}

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).

8. 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.

9.Explain different types of state in Compose and when to use each?

1. remember — Local component state

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

2. rememberSaveable — Survives configuration changes

@Composable
fun SearchField() {
    var query by rememberSaveable { mutableStateOf("") }

    TextField(
        value = query,
        onValueChange = { query = it },
        label = { Text("Search") }
    )
}

3. ViewModel State — Business logic state

class RideBookingViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(RideBookingUiState())
    val uiState = _uiState.asStateFlow()

    fun bookRide(destination: String) {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(isLoading = true)
            // Business logic...
        }
    }
}

@Composable
fun RideBookingScreen(viewModel: RideBookingViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()

    // UI based on uiState
}

4. State Hoisting — Shared state management

@Composable
fun StatefulCounter() {
    var count by remember { mutableStateOf(0) }
    StatelessCounter(count = count, onIncrement = { count++ })
}

@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit) {
    Button(onClick = onIncrement) {
        Text("Count: $count")
    }
}

Side Effects and Lifecycle

10. 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
    }
}

11. How do you handle complex state updates and side effects in Compose?

Complex State with Data Classes

data class RideBookingState(
    val pickup: String = "",
    val destination: String = "",
    val selectedRideType: RideType = RideType.GO,
    val isLoading: Boolean = false,
    val estimatedFare: Double? = null,
    val error: String? = null
)

@Composable
fun RideBookingScreen() {
    var state by remember { mutableStateOf(RideBookingState()) }

    // Update specific properties
    fun updateDestination(destination: String) {
        state = state.copy(destination = destination)
    }

    fun setLoading(loading: Boolean) {
        state = state.copy(isLoading = loading)
    }
}

Side Effects with LaunchedEffect

@Composable
fun LocationTracker(rideId: String) {
    var location by remember { mutableStateOf<Location?>(null) }

    // Side effect that runs when rideId changes
    LaunchedEffect(rideId) {
        locationService.trackLocation(rideId).collect { newLocation ->
            location = newLocation
        }
    }

    location?.let {
        MapView(location = it)
    }
}

DisposableEffect for Cleanup

@Composable
fun RideTrackingScreen(rideId: String) {
    DisposableEffect(rideId) {
        val listener = object : LocationListener {
            override fun onLocationChanged(location: Location) {
                // Handle location updates
            }
        }

        locationManager.requestLocationUpdates(listener)

        onDispose {
            locationManager.removeUpdates(listener)
        }
    }
}

12. 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")
    }
}

Architecture & Patterns

13. How do you implement MVVM architecture with Compose?

ViewModel Layer

@HiltViewModel
class RideBookingViewModel @Inject constructor(
    private val rideRepository: RideRepository,
    private val locationService: LocationService,
    private val fareCalculator: FareCalculator
) : ViewModel() {

    private val _uiState = MutableStateFlow(RideBookingUiState())
    val uiState = _uiState.asStateFlow()

    fun updateDestination(destination: String) {
        _uiState.value = _uiState.value.copy(destination = destination)
        calculateFare()
    }

    fun bookRide() {
        viewModelScope.launch {
            try {
                _uiState.value = _uiState.value.copy(isLoading = true)

                val currentLocation = locationService.getCurrentLocation()
                val ride = rideRepository.bookRide(
                    pickup = currentLocation,
                    destination = _uiState.value.destination,
                    rideType = _uiState.value.selectedRideType
                )

                _uiState.value = _uiState.value.copy(
                    isLoading = false,
                    bookedRide = ride
                )
            } catch (e: Exception) {
                _uiState.value = _uiState.value.copy(
                    isLoading = false,
                    error = e.message
                )
            }
        }
    }

    private fun calculateFare() {
        val state = _uiState.value
        if (state.pickup.isNotEmpty() && state.destination.isNotEmpty()) {
            viewModelScope.launch {
                val fare = fareCalculator.calculate(
                    pickup = state.pickup,
                    destination = state.destination,
                    rideType = state.selectedRideType
                )
                _uiState.value = _uiState.value.copy(estimatedFare = fare)
            }
        }
    }
}

data class RideBookingUiState(
    val pickup: String = "",
    val destination: String = "",
    val selectedRideType: RideType = RideType.GO,
    val estimatedFare: Fare? = null,
    val isLoading: Boolean = false,
    val bookedRide: Ride? = null,
    val error: String? = null
)

UI Layer

@Composable
fun RideBookingScreen(
    viewModel: RideBookingViewModel = hiltViewModel(),
    onRideBooked: (Ride) -> Unit
) {
    val uiState by viewModel.uiState.collectAsState()

    // Handle one-time events
    LaunchedEffect(uiState.bookedRide) {
        uiState.bookedRide?.let { ride ->
            onRideBooked(ride)
        }
    }

    RideBookingContent(
        uiState = uiState,
        onDestinationChange = viewModel::updateDestination,
        onRideTypeSelected = viewModel::updateRideType,
        onBookRide = viewModel::bookRide
    )
}

@Composable
private fun RideBookingContent(
    uiState: RideBookingUiState,
    onDestinationChange: (String) -> Unit,
    onRideTypeSelected: (RideType) -> Unit,
    onBookRide: () -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // Destination input
        OutlinedTextField(
            value = uiState.destination,
            onValueChange = onDestinationChange,
            label = { Text("Where to?") },
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(16.dp))

        // Ride type selection
        RideTypeSelector(
            selectedType = uiState.selectedRideType,
            onTypeSelected = onRideTypeSelected
        )

        // Fare estimate
        uiState.estimatedFare?.let { fare ->
            FareEstimateCard(fare = fare)
        }

        Spacer(modifier = Modifier.weight(1f))

        // Book ride button
        Button(
            onClick = onBookRide,
            enabled = !uiState.isLoading && uiState.destination.isNotEmpty(),
            modifier = Modifier.fillMaxWidth()
        ) {
            if (uiState.isLoading) {
                CircularProgressIndicator(
                    modifier = Modifier.size(16.dp),
                    color = MaterialTheme.colors.onPrimary
                )
            } else {
                Text("Book Ride")
            }
        }

        // Error handling
        uiState.error?.let { error ->
            Text(
                text = error,
                color = MaterialTheme.colors.error,
                modifier = Modifier.padding(top = 8.dp)
            )
        }
    }
}

Layouts and Advanced Components

14. 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)
        }
    }
}

Always provide a stable key parameter to avoid unnecessary recompositions when sorting or removing elements.

15. 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()
            }
        }
    }
}

16. 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

17. 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)
        }
    }
}

18. How do you handle navigation in Compose applications?

Navigation Setup

@Composable
fun CareerApp() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onBookRide = {
                    navController.navigate("ride_booking")
                }
            )
        }

        composable("ride_booking") {
            RideBookingScreen(
                onRideBooked = { ride ->
                    navController.navigate("ride_tracking/${ride.id}")
                },
                onBack = {
                    navController.popBackStack()
                }
            )
        }

        composable(
            route = "ride_tracking/{rideId}",
            arguments = listOf(
                navArgument("rideId") {
                    type = NavType.StringType
                }
            )
        ) { backStackEntry ->
            val rideId = backStackEntry.arguments?.getString("rideId") ?: ""
            RideTrackingScreen(
                rideId = rideId,
                onRideComplete = {
                    navController.navigate("home") {
                        popUpTo("home") { inclusive = false }
                    }
                }
            )
        }
    }
}

Type-Safe Navigation

// Define destinations
sealed class Screen(val route: String) {
    object Home : Screen("home")
    object RideBooking : Screen("ride_booking")
    data class RideTracking(val rideId: String) : Screen("ride_tracking/$rideId")
}

// Navigation extensions
fun NavController.navigateToRideTracking(rideId: String) {
    navigate(Screen.RideTracking(rideId).route)
}

// Usage
@Composable
fun RideBookingScreen(navController: NavController) {
    // ... other code

    Button(
        onClick = {
            navController.navigateToRideTracking(bookedRide.id)
        }
    ) {
        Text("Track Ride")
    }
}

Bottom Navigation

@Composable
fun MainScreen() {
    val navController = rememberNavController()
    val currentRoute by navController.currentBackStackEntryAsState()

    Scaffold(
        bottomBar = {
            BottomNavigation {
                bottomNavItems.forEach { screen ->
                    BottomNavigationItem(
                        selected = currentRoute?.destination?.route == screen.route,
                        onClick = {
                            navController.navigate(screen.route) {
                                popUpTo(navController.graph.startDestinationId)
                                launchSingleTop = true
                            }
                        },
                        icon = { Icon(screen.icon, contentDescription = null) },
                        label = { Text(screen.title) }
                    )
                }
            }
        }
    ) { paddingValues ->
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.padding(paddingValues)
        ) {
            // Define composables
        }
    }
}

19. 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

20. How do you optimize Compose performance and prevent unnecessary recompositions?

1. Stable Parameters

// Unstable - will always recompose
@Composable
fun UserCard(user: User, onClick: () -> Unit) {
    // onClick lambda is unstable
}
// Stable - can skip recomposition
@Composable
fun UserCard(
    user: User,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    // Use remember to stabilize lambda
    val stableClick = remember { onClick }

    Card(
        modifier = modifier.clickable { stableClick() }
    ) {
        Text(user.name)
    }
}

2. Stable Collections

// Unstable - List is mutable
@Composable
fun RidesList(rides: List<Ride>) {
    LazyColumn {
        items(rides) { ride ->
            RideItem(ride = ride)
        }
    }
}
// Stable - Use ImmutableList
@Composable
fun OptimizedRidesList(rides: ImmutableList<Ride>) {
    LazyColumn {
        items(rides) { ride ->
            RideItem(ride = ride)
        }
    }
}
// Or use keys for better performance
@Composable
fun RidesListWithKeys(rides: List<Ride>) {
    LazyColumn {
        items(rides, key = { it.id }) { ride ->
            RideItem(ride = ride)
        }
    }
}

3. Derivation and Calculations

@Composable
fun RideBookingScreen(rides: List<Ride>) {
    // Expensive calculation - recalculates on every recomposition
    val totalFare = rides.sumOf { it.fare }

    // Optimized - only recalculates when rides change
    val optimizedTotalFare = remember(rides) {
        rides.sumOf { it.fare }
    }

    // Even better - use derivedStateOf for reactive calculations
    val reactiveTotalFare by remember {
        derivedStateOf { rides.sumOf { it.fare } }
    }
}

4. Composition Local for Deep Hierarchies

val LocalTheme = compositionLocalOf<Theme> { error("No theme provided") }
@Composable
fun App() {
    CompositionLocalProvider(LocalTheme provides DarkTheme) {
        MainScreen()
    }
}
@Composable
fun DeepNestedComponent() {
    val theme = LocalTheme.current
    // Use theme without prop drilling
}

21. Explain LazyColumn/LazyRow optimization techniques.

1. Using Keys for Item Identity

@Composable
fun RideHistoryList(rides: List<Ride>) {
    LazyColumn {
        items(
            items = rides,
            key = { ride -> ride.id } // Stable identifier
        ) { ride ->
            RideHistoryItem(
                ride = ride,
                modifier = Modifier.animateItemPlacement() // Smooth animations
            )
        }
    }
}

2. Content Types for ViewHolder Reuse

@Composable
fun MixedContentList(items: List<ListItem>) {
    LazyColumn {
        items(
            items = items,
            key = { it.id },
            contentType = { item ->
                when (item) {
                    is RideItem -> "ride"
                    is AdItem -> "ad"
                    is HeaderItem -> "header"
                    else -> "default"
                }
            }
        ) { item ->
            when (item) {
                is RideItem -> RideCard(item)
                is AdItem -> AdBanner(item)
                is HeaderItem -> SectionHeader(item)
            }
        }
    }
}

3. Prefetch and Content Padding

@Composable
fun OptimizedRidesList(rides: List<Ride>) {
    val listState = rememberLazyListState()

    LazyColumn(
        state = listState,
        contentPadding = PaddingValues(16.dp), // Better than wrapping in padding
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(rides, key = { it.id }) { ride ->
            RideItem(ride = ride)
        }
    }
}

4. Custom Layout for Complex Items

@Composable
fun RideItem(ride: Ride) {
    Layout(
        content = {
            Text(ride.destination)
            Text(ride.fare.toString())
            AsyncImage(
                model = ride.driverPhoto,
                contentDescription = null
            )
        }
    ) { measurables, constraints ->
        // Custom measurement and placement logic
        val textPlaceable = measurables[0].measure(constraints)
        val farePlaceable = measurables[1].measure(constraints)
        val imagePlaceable = measurables[2].measure(
            constraints.copy(maxWidth = 100.dp.roundToPx())
        )

        val height = maxOf(textPlaceable.height, imagePlaceable.height)

        layout(constraints.maxWidth, height) {
            imagePlaceable.place(0, 0)
            textPlaceable.place(imagePlaceable.width + 16.dp.roundToPx(), 0)
            farePlaceable.place(
                constraints.maxWidth - farePlaceable.width,
                (height - farePlaceable.height) / 2
            )
        }
    }
}

22. 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)
            }
        }
    }
}

23. 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.

24. 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

25. 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
    )
}

Testing in Compose

26. How to test composables?

Compose provides a testing library with ComposeTestRule for UI tests and semantic assertions.

Basic UI Testing

@RunWith(AndroidJUnit4::class)
class RideBookingScreenTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun destinationInput_enablesBookButton() {
        // Given
        composeTestRule.setContent {
            RideBookingScreen(
                uiState = RideBookingUiState(),
                onDestinationChange = {},
                onBookRide = {}
            )
        }

        // When - Enter destination
        composeTestRule
            .onNodeWithText("Where to?")
            .performTextInput("Dubai Mall")

        // Then - Book button should be enabled
        composeTestRule
            .onNodeWithText("Book Ride")
            .assertIsEnabled()
    }

    @Test
    fun loadingState_showsProgressIndicator() {
        // Given
        val loadingState = RideBookingUiState(
            isLoading = true,
            destination = "Business Bay"
        )

        // When
        composeTestRule.setContent {
            RideBookingScreen(
                uiState = loadingState,
                onDestinationChange = {},
                onBookRide = {}
            )
        }

        // Then
        composeTestRule
            .onNodeWithContentDescription("Loading")
            .assertIsDisplayed()

        composeTestRule
            .onNodeWithText("Book Ride")
            .assertIsNotEnabled()
    }
}
@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.

Testing Lists

@Test
fun ridesList_displaysAllRides() {
    val rides = listOf(
        Ride("1", "Dubai Mall", RideStatus.COMPLETED),
        Ride("2", "Business Bay", RideStatus.CANCELLED),
        Ride("3", "JBR", RideStatus.COMPLETED)
    )

    composeTestRule.setContent {
        RideHistoryScreen(rides = rides)
    }

    // Verify all rides are displayed
    rides.forEach { ride ->
        composeTestRule
            .onNodeWithText(ride.destination)
            .assertIsDisplayed()
    }
}

Testing User Interactions

@Test
fun rideTypeSelection_updatesSelectedType() {
    var selectedType = RideType.GO

    composeTestRule.setContent {
        RideTypeSelector(
            selectedType = selectedType,
            onTypeSelected = { selectedType = it }
        )
    }

    // Select different ride type
    composeTestRule
        .onNodeWithText("Bike")
        .performClick()

    assertEquals(RideType.BIKE, selectedType)
}

Testing with ViewModels

@Test
fun bookRide_callsViewModelMethod() {
    val viewModel = mockk<RideBookingViewModel>(relaxed = true)
    every { viewModel.uiState } returns MutableStateFlow(RideBookingUiState()).asStateFlow()

    composeTestRule.setContent {
        RideBookingScreen(viewModel = viewModel)
    }

    // Enter destination and book ride
    composeTestRule
        .onNodeWithText("Where to?")
        .performTextInput("Dubai Mall")

    composeTestRule
        .onNodeWithText("Book Ride")
        .performClick()

    verify { viewModel.bookRide() }
}

Advanced Topics

27. 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.

28. How do you implement custom layouts and animations in Compose?

Custom Layout

@Composable
fun RideCardLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        require(measurables.size >= 3) { "RideCardLayout requires at least 3 children" }

        val imageConstraints = constraints.copy(
            maxWidth = 80.dp.roundToPx(),
            maxHeight = 80.dp.roundToPx()
        )

        // Measure children
        val imagePlaceable = measurables[0].measure(imageConstraints)
        val titlePlaceable = measurables[1].measure(
            constraints.copy(maxWidth = constraints.maxWidth - imagePlaceable.width - 16.dp.roundToPx())
        )
        val subtitlePlaceable = measurables[2].measure(
            constraints.copy(maxWidth = constraints.maxWidth - imagePlaceable.width - 16.dp.roundToPx())
        )

        val totalHeight = maxOf(
            imagePlaceable.height,
            titlePlaceable.height + subtitlePlaceable.height + 8.dp.roundToPx()
        )

        layout(constraints.maxWidth, totalHeight) {
            // Place image on left
            imagePlaceable.place(0, 0)

            // Place title and subtitle on right
            val textStartX = imagePlaceable.width + 16.dp.roundToPx()
            titlePlaceable.place(textStartX, 0)
            subtitlePlaceable.place(
                textStartX, 
                titlePlaceable.height + 8.dp.roundToPx()
            )
        }
    }
}

Animations

@Composable
fun AnimatedRideStatusCard(rideStatus: RideStatus) {
    val animatedColor by animateColorAsState(
        targetValue = when (rideStatus) {
            RideStatus.REQUESTED -> Color.Orange
            RideStatus.DRIVER_ASSIGNED -> Color.Blue
            RideStatus.PICKUP -> Color.Green
            RideStatus.COMPLETED -> Color.Gray
        },
        animationSpec = tween(durationMillis = 300)
    )

    val scale by animateFloatAsState(
        targetValue = if (rideStatus == RideStatus.PICKUP) 1.1f else 1f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )

    Card(
        backgroundColor = animatedColor,
        modifier = Modifier
            .scale(scale)
            .padding(8.dp)
    ) {
        Text(
            text = rideStatus.name,
            modifier = Modifier.padding(16.dp)
        )
    }
}

Transition Animations

@Composable
fun RideBookingFlow() {
    var currentStep by remember { mutableStateOf(BookingStep.DESTINATION) }

    AnimatedContent(
        targetState = currentStep,
        transitionSpec = {
            slideInHorizontally { width -> width } + fadeIn() with
            slideOutHorizontally { width -> -width } + fadeOut()
        }
    ) { step ->
        when (step) {
            BookingStep.DESTINATION -> DestinationScreen(
                onNext = { currentStep = BookingStep.RIDE_TYPE }
            )
            BookingStep.RIDE_TYPE -> RideTypeScreen(
                onNext = { currentStep = BookingStep.CONFIRMATION }
            )
            BookingStep.CONFIRMATION -> ConfirmationScreen()
        }
    }
}

29. How do you handle themes and styling in Compose?

Custom Theme Implementation

// Define colors
private val DarkColorPalette = darkColors(
    primary = Color(0xFF00C853),      // Careem green
    primaryVariant = Color(0xFF00A843),
    secondary = Color(0xFFFFC107),     // Careem yellow
    background = Color(0xFF121212),
    surface = Color(0xFF1E1E1E),
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onBackground = Color.White,
    onSurface = Color.White
)

private val LightColorPalette = lightColors(
    primary = Color(0xFF00C853),
    primaryVariant = Color(0xFF00A843),
    secondary = Color(0xFFFFC107),
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black
)

// Custom typography
val CareemTypography = Typography(
    h1 = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Bold,
        fontSize = 28.sp,
        lineHeight = 32.sp
    ),
    h2 = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.SemiBold,
        fontSize = 24.sp,
        lineHeight = 28.sp
    ),
    body1 = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 20.sp
    )
)

// Theme composable
@Composable
fun CareemTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = CareemTypography,
        shapes = Shapes(
            small = RoundedCornerShape(8.dp),
            medium = RoundedCornerShape(12.dp),
            large = RoundedCornerShape(16.dp)
        ),
        content = content
    )
}

// Custom theme extensions
object CareemTheme {
    val colors: Colors
        @Composable
        get() = MaterialTheme.colors

    val typography: Typography
        @Composable
        get() = MaterialTheme.typography

    val shapes: Shapes
        @Composable
        get() = MaterialTheme.shapes
}

// Usage
@Composable
fun RideCard(ride: Ride) {
    Card(
        shape = CareemTheme.shapes.medium,
        backgroundColor = CareemTheme.colors.surface,
        elevation = 4.dp
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = ride.destination,
                style = CareemTheme.typography.h2,
                color = CareemTheme.colors.onSurface
            )
            Text(
                text = "AED ${ride.fare}",
                style = CareemTheme.typography.body1,
                color = CareemTheme.colors.primary
            )
        }
    }
}

Custom Composition Locals

// Custom theme properties
data class CareemColors(
    val success: Color,
    val warning: Color,
    val info: Color,
    val rideGo: Color,
    val rideBike: Color,
    val ridePlus: Color
)

private val LocalCareemColors = compositionLocalOf {
    CareemColors(
        success = Color.Green,
        warning = Color.Orange,
        info = Color.Blue,
        rideGo = Color(0xFF00C853),
        rideBike = Color(0xFF2196F3),
        ridePlus = Color(0xFF9C27B0)
    )
}

@Composable
fun CareemTheme(
    colors: CareemColors = CareemColors(
        success = Color(0xFF4CAF50),
        warning = Color(0xFFFF9800),
        info = Color(0xFF2196F3),
        rideGo = Color(0xFF00C853),
        rideBike = Color(0xFF03A9F4),
        ridePlus = Color(0xFF673AB7)
    ),
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalCareemColors provides colors
    ) {
        MaterialTheme(content = content)
    }
}

// Access custom colors
@Composable
fun RideTypeButton(rideType: RideType, onClick: () -> Unit) {
    val careemColors = LocalCareemColors.current

    val backgroundColor = when (rideType) {
        RideType.GO -> careemColors.rideGo
        RideType.BIKE -> careemColors.rideBike
        RideType.PLUS -> careemColors.ridePlus
    }

    Button(
        onClick = onClick,
        colors = ButtonDefaults.buttonColors(backgroundColor = backgroundColor)
    ) {
        Text(rideType.name)
    }
}

30. How do you handle complex UI state and loading states in Compose?

Comprehensive State Management

sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val exception: Throwable) : UiState<Nothing>()
}

// Usage in ViewModel
@HiltViewModel
class RideHistoryViewModel @Inject constructor(
    private val rideRepository: RideRepository
) : ViewModel() {

    private val _ridesState = MutableStateFlow<UiState<List<Ride>>>(UiState.Loading)
    val ridesState = _ridesState.asStateFlow()

    init {
        loadRides()
    }

    private fun loadRides() {
        viewModelScope.launch {
            try {
                _ridesState.value = UiState.Loading
                val rides = rideRepository.getUserRides()
                _ridesState.value = UiState.Success(rides)
            } catch (e: Exception) {
                _ridesState.value = UiState.Error(e)
            }
        }
    }

    fun retry() {
        loadRides()
    }
}

// UI with different states
@Composable
fun RideHistoryScreen(viewModel: RideHistoryViewModel = hiltViewModel()) {
    val ridesState by viewModel.ridesState.collectAsState()

    when (ridesState) {
        is UiState.Loading -> {
            LoadingScreen()
        }
        is UiState.Success -> {
            RideHistoryList(rides = ridesState.data)
        }
        is UiState.Error -> {
            ErrorScreen(
                error = ridesState.exception,
                onRetry = viewModel::retry
            )
        }
    }
}

@Composable
private fun LoadingScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            CircularProgressIndicator(
                color = MaterialTheme.colors.primary
            )
            Spacer(modifier = Modifier.height(16.dp))
            Text("Loading your rides...")
        }
    }
}

@Composable
private fun ErrorScreen(error: Throwable, onRetry: () -> Unit) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Icon(
            imageVector = Icons.Default.Error,
            contentDescription = null,
            tint = MaterialTheme.colors.error,
            modifier = Modifier.size(64.dp)
        )

        Spacer(modifier = Modifier.height(16.dp))

        Text(
            text = "Something went wrong",
            style = MaterialTheme.typography.h6
        )

        Text(
            text = error.message ?: "Unknown error occurred",
            style = MaterialTheme.typography.body2,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f),
            textAlign = TextAlign.Center
        )

        Spacer(modifier = Modifier.height(24.dp))

        Button(onClick = onRetry) {
            Text("Try Again")
        }
    }
}

Pagination and Infinite Scroll

@HiltViewModel
class RideHistoryViewModel @Inject constructor(
    private val rideRepository: RideRepository
) : ViewModel() {

    private val _rides = MutableStateFlow<List<Ride>>(emptyList())
    val rides = _rides.asStateFlow()

    private val _isLoading = MutableStateFlow(false)
    val isLoading = _isLoading.asStateFlow()

    private val _hasMoreData = MutableStateFlow(true)
    val hasMoreData = _hasMoreData.asStateFlow()

    private var currentPage = 0
    private val pageSize = 20

    init {
        loadNextPage()
    }

    fun loadNextPage() {
        if (_isLoading.value || !_hasMoreData.value) return

        viewModelScope.launch {
            try {
                _isLoading.value = true

                val newRides = rideRepository.getRides(
                    page = currentPage,
                    pageSize = pageSize
                )

                if (newRides.isEmpty()) {
                    _hasMoreData.value = false
                } else {
                    _rides.value = _rides.value + newRides
                    currentPage++
                }
            } catch (e: Exception) {
                // Handle error
            } finally {
                _isLoading.value = false
            }
        }
    }
}

@Composable
fun PaginatedRidesList(viewModel: RideHistoryViewModel = hiltViewModel()) {
    val rides by viewModel.rides.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()
    val hasMoreData by viewModel.hasMoreData.collectAsState()

    val listState = rememberLazyListState()

    // Load more when reaching end
    LaunchedEffect(listState) {
        snapshotFlow { listState.layoutInfo.visibleItemsInfo }
            .collect { visibleItems ->
                val totalItems = rides.size
                val lastVisibleItem = visibleItems.lastOrNull()?.index ?: 0

                if (lastVisibleItem >= totalItems - 3 && hasMoreData && !isLoading) {
                    viewModel.loadNextPage()
                }
            }
    }

    LazyColumn(state = listState) {
        items(rides, key = { it.id }) { ride ->
            RideHistoryItem(ride = ride)
        }

        if (isLoading) {
            item {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp),
                    contentAlignment = Alignment.Center
                ) {
                    CircularProgressIndicator()
                }
            }
        }
    }
}

Interoperability and Migration

31. How do you integrate Compose with existing View-based code?

Using Compose in Traditional Views

// In XML layout
<androidx.compose.ui.platform.ComposeView
    android:id="@+id/compose_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

// In Fragment/Activity
class RideDetailsFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply {
            setContent {
                CareemTheme {
                    RideDetailsScreen(rideId = args.rideId)
                }
            }
        }
    }
}

// Mixed content
class MixedContentActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_mixed)

        val composeView = findViewById<ComposeView>(R.id.compose_section)
        composeView.setContent {
            CareemTheme {
                RideBookingSection(
                    onRideBooked = { ride ->
                        // Interact with traditional views
                        updateTraditionalViews(ride)
                    }
                )
            }
        }
    }
}

Using Views in Compose

@Composable
fun MapScreen(location: Location) {
    AndroidView(
        factory = { context ->
            MapView(context).apply {
                // Initialize map
                onCreate(null)
                getMapAsync { googleMap ->
                    // Setup map
                }
            }
        },
        update = { mapView ->
            // Update map when location changes
            mapView.getMapAsync { googleMap ->
                googleMap.moveCamera(
                    CameraUpdateFactory.newLatLngZoom(
                        LatLng(location.latitude, location.longitude),
                        15f
                    )
                )
            }
        },
        modifier = Modifier.fillMaxSize()
    )
}

// Custom View integration
@Composable
fun CustomChartView(data: ChartData) {
    AndroidView(
        factory = { context ->
            CustomChartView(context).apply {
                layoutParams = ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT
                )
            }
        },
        update = { chartView ->
            chartView.updateData(data)
        }
    )
}

Gradual Migration Strategy

// Step 1: Start with leaf components
@Composable
fun RideCard(ride: Ride) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        // Compose implementation
    }
}

// Step 2: Migrate screens gradually
@Composable
fun RideHistoryScreen() {
    LazyColumn {
        items(rides) { ride ->
            RideCard(ride = ride) // New Compose component
        }
    }
}

// Step 3: Eventually migrate entire flows
@Composable
fun RideBookingFlow() {
    val navController = rememberNavController()
    // Full Compose navigation
}

Common Pitfalls and Best Practices

32. What are common mistakes developers make when starting with Compose?

1. Recomposition Performance Issues

// ❌ Bad - Unstable parameters cause unnecessary recomposition
@Composable
fun UserList(users: List<User>, onClick: (User) -> Unit) {
    LazyColumn {
        items(users) { user ->
            UserCard(
                user = user,
                onClick = { onClick(user) } // New lambda on every recomposition
            )
        }
    }
}

// ✅ Good - Stable parameters with keys
@Composable
fun UserList(
    users: List<User>,
    onClick: (User) -> Unit,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier = modifier) {
        items(
            items = users,
            key = { user -> user.id } // Stable key
        ) { user ->
            UserCard(
                user = user,
                onClick = remember { { onClick(user) } } // Stable lambda
            )
        }
    }
}

2. Incorrect State Management

// ❌ Bad - Mutating state directly
@Composable
fun Counter() {
    var count = 0 // This won't trigger recomposition

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

// ✅ Good - Using remember and mutableStateOf
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

3. Side Effects Misuse

// ❌ Bad - Side effect in composition
@Composable
fun UserProfile(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }

    // This runs on every recomposition!
    viewModelScope.launch {
        user = userRepository.getUser(userId)
    }

    user?.let { UserCard(it) }
}

// ✅ Good - Using LaunchedEffect
@Composable
fun UserProfile(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }

    LaunchedEffect(userId) {
        user = userRepository.getUser(userId)
    }

    user?.let { UserCard(it) }
}

4. Memory Leaks with Lifecycle

// ❌ Bad - Not handling lifecycle properly
@Composable
fun LocationTracker() {
    DisposableEffect(Unit) {
        locationManager.startLocationUpdates()

        onDispose {
            // Forgot to stop location updates - memory leak!
        }
    }
}

// ✅ Good - Proper cleanup
@Composable
fun LocationTracker() {
    DisposableEffect(Unit) {
        val callback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                // Handle location
            }
        }

        locationManager.requestLocationUpdates(callback)

        onDispose {
            locationManager.removeLocationUpdates(callback)
        }
    }
}

33. What are the best practices for organizing Compose code?

Project Structure

app/
├── src/main/java/com/careem/
│   ├── ui/
│   │   ├── theme/
│   │   │   ├── Color.kt
│   │   │   ├── Theme.kt
│   │   │   ├── Type.kt
│   │   │   └── Shape.kt
│   │   ├── components/
│   │   │   ├── buttons/
│   │   │   ├── cards/
│   │   │   └── inputs/
│   │   ├── screens/
│   │   │   ├── home/
│   │   │   ├── booking/
│   │   │   └── profile/
│   │   └── navigation/
│   ├── viewmodel/
│   ├── data/
│   └── di/

Component Organization

// 1. Screen-level composables
@Composable
fun RideBookingScreen(
    viewModel: RideBookingViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsState()

    RideBookingContent(
        uiState = uiState,
        onEvent = viewModel::handleEvent
    )
}

// 2. Content composables (stateless)
@Composable
private fun RideBookingContent(
    uiState: RideBookingUiState,
    onEvent: (RideBookingEvent) -> Unit
) {
    Column {
        // UI implementation
    }
}

// 3. Reusable components
@Composable
fun RideCard(
    ride: Ride,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    // Component implementation
}

// 4. Preview functions
@Preview
@Composable
private fun RideBookingScreenPreview() {
    CareemTheme {
        RideBookingContent(
            uiState = RideBookingUiState(),
            onEvent = {}
        )
    }
}

메타데이터
post_id
db6738ba36da
slug
jetpack-compose-interview-questions-in-2026-db6738ba36da
url
https://medium.com/@abhiappmobiledeveloper/jetpack-compose-interview-questions-in-2026-db6738ba36da
canonical_url
https://medium.com/@abhiappmobiledeveloper/jetpack-compose-interview-questions-in-2026-db6738ba36da
author_url
https://medium.com/@abhiappmobiledeveloper
status
ok
fetched_at
2026-08-21 04:22:37