> For the complete documentation index, see [llms.txt](https://notes.tejpratapsingh.com/_/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://notes.tejpratapsingh.com/_/android-tips/interview/compose/compose-part-3.md).

# Compose Part 3

#### **Q21. What tools do you use to measure Compose frame times and recomposition counts in production?**

In production, you cannot attach Android Studio profilers, so you rely on runtime APIs and CI-driven benchmarks. **JankStats** (from the AndroidX JankStats library) is the primary production tool: it registers a `OnFrameMetricsAvailableListener` via `Choreographer` to report slow frames and jank directly from user devices, allowing you to upload telemetry to Firebase Crashlytics or your analytics backend. For pre-release measurement, **Macrobenchmark** with `FrameTimingMetric` runs automated UI interactions and reports `frameOverrunMs` and `frameDurationCpuMs` in a controlled environment. To track recompositions, you cannot use the Layout Inspector in production, but you can embed **Composition tracing** by enabling the Compose Compiler's `reportsDestination` in CI to generate skippability reports. Additionally, you can wrap critical composables with `Trace.beginSection`/`endSection` and use `Perfetto` or Android's system tracing to visualize frame budgets. For a lightweight in-app overlay, some teams build a custom `Choreographer.FrameCallback` that calculates the time between `doFrame` callbacks and logs frames exceeding 16.6ms (60fps) or 11.1ms (120fps).

```kotlin
// Production jank tracking using JankStats
class JankMonitor(context: Context) {
    private val jankStats = JankStats.createAndTrack(context.window) { frameData ->
        if (frameData.isJank) {
            // Report to analytics: frameData.frameDurationUiNanos / 1_000_000.0
            Log.w("JankStats", "Jank detected: ${frameData.frameDurationUiNanos / 1_000_000.0}ms")
        }
    }

    fun start() { jankStats.isTrackingEnabled = true }
    fun stop() { jankStats.isTrackingEnabled = false }
}

// Macrobenchmark for CI/Pre-production
class ScrollBenchmark {
    @get:Rule val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun scrollList() = benchmarkRule.measureRepeated(
        packageName = "com.example.app",
        metrics = listOf(FrameTimingMetric(), CpuUsageMetric()),
        iterations = 5,
        setupBlock = { pressHome(); startActivityAndWait() }
    ) {
        val list = device.findObject(By.res("lazy_list"))
        list.setGestureMargin(device.displayWidth / 5)
        list.fling(Direction.DOWN)
    }
}

// Custom frame callback overlay (debug builds only)
val frameCallback = object : Choreographer.FrameCallback {
    private var lastFrame = 0L
    override fun doFrame(frameTimeNanos: Long) {
        if (lastFrame != 0L) {
            val diffMs = (frameTimeNanos - lastFrame) / 1_000_000.0
            if (diffMs > 17) Log.d("FrameDrop", "Frame took ${diffMs}ms")
        }
        lastFrame = frameTimeNanos
        Choreographer.getInstance().postFrameCallback(this)
    }
}
```

***

#### **Q22. How would you build a custom layout using `Layout` composable? What are `MeasurePolicy` and `IntrinsicMeasurable`?**

The `Layout` composable is the primitive for building custom layouts in Compose. It takes a list of children (`content`), a `Modifier`, and a `MeasurePolicy` lambda that defines how to measure and place those children. Inside the lambda, you receive `measurables: List<Measurable>` and `constraints: Constraints`. You call `measurable.measure(constraints)` on each child to get a `Placeable`, then call `layout(width, height)` to declare your own size, and inside its block call `placeable.placeRelative(x, y)` to position children. `IntrinsicMeasurable` (accessed via `Measurable` which extends it) allows the parent to query a child's intrinsic dimensions before measuring it. This is essential when your layout needs to support `Modifier.width(IntrinsicSize.Min)` or similar. The intrinsic methods (`minIntrinsicWidth`, `maxIntrinsicWidth`, etc.) let you ask, "How wide would you be if I gave you infinite height?" without actually committing to a measurement.

```kotlin
// A custom layout that places children in a simple Flow-like row with wrapping.
@Composable
fun FlowRow(
    modifier: Modifier = Modifier,
    horizontalGap: Dp = 8.dp,
    verticalGap: Dp = 8.dp,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        val hGapPx = horizontalGap.roundToPx()
        val vGapPx = verticalGap.roundToPx()

        val rows = mutableListOf<List<Placeable>>()
        val rowWidths = mutableListOf<Int>()
        val rowHeights = mutableListOf<Int>()

        var currentRow = mutableListOf<Placeable>()
        var currentRowWidth = 0
        var currentRowHeight = 0

        measurables.forEach { measurable ->
            val placeable = measurable.measure(Constraints())

            if (currentRow.isNotEmpty() && currentRowWidth + hGapPx + placeable.width > constraints.maxWidth) {
                rows.add(currentRow)
                rowWidths.add(currentRowWidth)
                rowHeights.add(currentRowHeight)
                currentRow = mutableListOf()
                currentRowWidth = 0
                currentRowHeight = 0
            }

            currentRow.add(placeable)
            currentRowWidth += if (currentRow.size > 1) hGapPx else 0 + placeable.width
            currentRowHeight = maxOf(currentRowHeight, placeable.height)
        }

        if (currentRow.isNotEmpty()) {
            rows.add(currentRow)
            rowWidths.add(currentRowWidth)
            rowHeights.add(currentRowHeight)
        }

        val totalHeight = rowHeights.sum() + (rows.size - 1).coerceAtLeast(0) * vGapPx
        val maxWidth = rowWidths.maxOrNull()?.coerceIn(constraints.minWidth, constraints.maxWidth) ?: constraints.minWidth

        layout(maxWidth, totalHeight.coerceIn(constraints.minHeight, constraints.maxHeight)) {
            var y = 0
            rows.forEachIndexed { rowIndex, row ->
                var x = 0
                row.forEachIndexed { index, placeable ->
                    if (index > 0) x += hGapPx
                    placeable.placeRelative(x, y)
                    x += placeable.width
                }
                y += rowHeights[rowIndex] + vGapPx
            }
        }
    }
}
```

***

#### **Q23. What is the difference between `SubcomposeLayout` and the standard `Layout` composable?**

`Layout` follows the standard Compose pipeline: **composition** happens first (the entire content lambda executes to build the UI tree), then **measurement** happens (the `MeasurePolicy` measures and places the already-composed children). This is efficient because composition and measurement are cleanly separated. `SubcomposeLayout` breaks this separation: it allows you to call `subcompose(slotId) { content() }` *inside* the measure/place block. This means you can decide what to compose based on the size or constraints available during measurement. The trade-off is performance: `SubcomposeLayout` triggers an additional composition pass during measurement, which is significantly more expensive and can cause frame drops if used inside scrollable containers or large lists. Use `Layout` for almost everything (custom rows, columns, grids). Reserve `SubcomposeLayout` only for cases where the content genuinely depends on measurement results, such as a `BoxWithConstraints` (which is built on `SubcomposeLayout`), a measured overlay, or a layout that needs to measure one child to determine how many other children to compose.

```kotlin
// Standard Layout: composition happens first, then measure/place.
@Composable
fun SimpleColumn(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    Layout(content = content, modifier = modifier) { measurables, constraints ->
        var y = 0
        val placeables = measurables.map { it.measure(constraints) }
        val maxWidth = placeables.maxOfOrNull { it.width } ?: 0
        val totalHeight = placeables.sumOf { it.height }
        layout(maxWidth, totalHeight) {
            placeables.forEach {
                it.placeRelative(0, y)
                y += it.height
            }
        }
    }
}

// SubcomposeLayout: compose during measurement based on measured results.
@Composable
fun MeasuredDependentLayout(
    modifier: Modifier = Modifier,
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (Size) -> Unit
) {
    SubcomposeLayout(modifier = modifier) { constraints ->
        // First, compose and measure the main content.
        val mainMeasurables = subcompose("main", mainContent)
        val mainPlaceables = mainMeasurables.map { it.measure(constraints) }
        val maxMainWidth = mainPlaceables.maxOfOrNull { it.width } ?: 0
        val maxMainHeight = mainPlaceables.maxOfOrNull { it.height } ?: 0

        // Now compose the dependent content using the measured size.
        val dependentMeasurables = subcompose("dependent") {
            dependentContent(Size(maxMainWidth.toDp().value, maxMainHeight.toDp().value))
        }
        val dependentPlaceables = dependentMeasurables.map {
            it.measure(Constraints(maxWidth = maxMainWidth, maxHeight = maxMainHeight))
        }

        layout(maxMainWidth, maxMainHeight) {
            mainPlaceables.forEach { it.placeRelative(0, 0) }
            dependentPlaceables.forEach { it.placeRelative(0, 0) }
        }
    }
}
```

***

#### **Q24. How do you create a custom `Modifier` using `Modifier.Node` versus the older `Modifier.composed` approach?**

`Modifier.composed` was the original API for stateful modifiers. It creates a new `Composition` for every element the modifier is applied to, which is flexible but expensive because each element gets its own composer, recomposition scope, and allocation overhead. `Modifier.Node` (introduced in Compose UI 1.5.0) is the modern, high-performance replacement. It creates a node directly in the modifier tree without a separate composition, allowing the node to be reused across recompositions and enabling much lower allocation pressure. To create a `Modifier.Node`, you define a class extending `Modifier.Node` and implementing one or more node type interfaces (e.g., `LayoutModifierNode`, `DrawModifierNode`, `PointerInputModifierNode`). You then create a `ModifierNodeElement` that acts as the immutable data holder (like `ModifierData` in the old system) and implements `create()` and `update()`. The framework calls `update()` when the same element is recomposed with new parameters, allowing you to mutate the existing node instead of creating a new one.

```kotlin
// Modern Modifier.Node approach: A custom modifier that draws a border with animated color.
class AnimatedBorderNode(var color: Color, var width: Dp) : Modifier.Node(), DrawModifierNode {
    override fun ContentDrawScope.draw() {
        drawContent()
        drawRect(
            color = color,
            style = Stroke(width = width.toPx()),
            size = size
        )
    }
}

class AnimatedBorderElement(
    private val color: Color,
    private val width: Dp
) : ModifierNodeElement<AnimatedBorderNode>() {
    override fun create() = AnimatedBorderNode(color, width)
    override fun update(node: AnimatedBorderNode) {
        node.color = color
        node.width = width
    }
    override fun hashCode() = 31 * color.hashCode() + width.hashCode()
    override fun equals(other: Any?) = other is AnimatedBorderElement && other.color == color && other.width == width
}

fun Modifier.animatedBorder(color: Color, width: Dp = 2.dp) = this then AnimatedBorderElement(color, width)

// Legacy Modifier.composed approach (higher overhead, avoid in new code):
fun Modifier.legacyBorder(color: State<Color>, width: Dp) = composed {
    val animatedColor by animateColorAsState(color.value)
    Modifier.drawBehind {
        drawRect(color = animatedColor, style = Stroke(width = width.toPx()))
    }
}

// Usage
@Composable
fun CardWithBorder() {
    val borderColor by animateColorAsState(if (isSelected) Color.Blue else Color.Gray)
    Box(
        modifier = Modifier
            .size(100.dp)
            .animatedBorder(color = borderColor, width = 4.dp)
    )
}
```

***

#### **Q25. Explain how `DrawScope` and `Canvas` work in Compose — how do you handle touch events on custom drawn elements?**

`DrawScope` is the receiver for all custom drawing in Compose (via `Canvas` composable or `Modifier.drawBehind`/`drawWithContent`). It provides a density-aware drawing environment where you can call methods like `drawRect`, `drawCircle`, `drawPath`, etc., using `Dp` values that are automatically converted to pixels. The `Canvas` composable creates a native Android `android.graphics.Canvas` backing and exposes it through `DrawScope`. Unlike traditional View-based `onDraw(Canvas)`, Compose drawing is stateless and recomposes automatically if the drawing parameters change. To handle touch events on custom drawn elements, you do **not** use `onTouchEvent` like in Views; instead, you wrap the `Canvas` with `Modifier.pointerInput` and use gesture detectors like `detectTapGestures`, `detectDragGestures`, or the low-level `awaitPointerEventScope`. You must manually map pointer coordinates to your drawn geometry to determine hits (e.g., checking if a touch falls inside a circle or path). For complex interactive drawings, maintain the geometry state in a `ViewModel` or plain class and update it from the gesture block.

```kotlin
@Composable
fun InteractivePieChart(
    slices: List<PieSlice>,
    modifier: Modifier = Modifier,
    onSliceClick: (PieSlice) -> Unit
) {
    var selectedIndex by remember { mutableIntStateOf(-1) }
    val total = remember(slices) { slices.sumOf { it.value }.toFloat() }

    Canvas(
        modifier = modifier
            .fillMaxSize()
            .pointerInput(slices) {
                detectTapGestures { offset ->
                    val center = Offset(size.width / 2f, size.height / 2f)
                    val touchVector = offset - center
                    val touchAngle = (touchVector.getAngle() + 360) % 360
                    val touchRadius = touchVector.getDistance()

                    var startAngle = 0f
                    slices.forEachIndexed { index, slice ->
                        val sweep = (slice.value / total) * 360f
                        if (touchAngle in startAngle..(startAngle + sweep) && touchRadius <= size.minDimension / 2f) {
                            selectedIndex = if (selectedIndex == index) -1 else index
                            onSliceClick(slice)
                        }
                        startAngle += sweep
                    }
                }
            }
    ) {
        val radius = size.minDimension / 2f
        val center = Offset(size.width / 2f, size.height / 2f)
        var startAngle = 0f

        slices.forEachIndexed { index, slice ->
            val sweep = (slice.value / total) * 360f
            val isSelected = index == selectedIndex

            drawArc(
                color = slice.color,
                startAngle = startAngle,
                sweepAngle = sweep - 2f, // 2f gap between slices
                useCenter = true,
                topLeft = center - Offset(radius, radius),
                size = Size(radius * 2, radius * 2),
                style = Fill
            )

            if (isSelected) {
                drawArc(
                    color = Color.Black.copy(alpha = 0.2f),
                    startAngle = startAngle,
                    sweepAngle = sweep - 2f,
                    useCenter = true,
                    topLeft = center - Offset(radius, radius),
                    size = Size(radius * 2, radius * 2),
                    style = Fill
                )
            }
            startAngle += sweep
        }
    }
}

data class PieSlice(val value: Double, val color: Color, val label: String)

// Extension helpers
private fun Offset.getAngle(): Float = kotlin.math.atan2(y, x) * (180f / kotlin.math.PI.toFloat())
private fun Offset.getDistance(): Float = kotlin.math.hypot(x, y)
```

***

#### **Q26. How would you implement a staggered grid layout before `LazyVerticalStaggeredGrid` existed in Compose?**

Before `LazyVerticalStaggeredGrid` was introduced in Compose Foundation 1.3, you had to build your own layout. The most practical approach was to use a custom `Layout` that measured all items and distributed them into a fixed number of columns, tracking the cumulative height of each column to place the next item in the shortest one. This is a "masonry" layout. Since it was pre-lazy, this early approach usually measured all children at once, making it suitable for moderate-sized lists but not for infinite scrolling. For a lazy version, you would have to build a custom `LazyLayout` (the internal primitive behind `LazyColumn`), which is significantly more complex and requires managing item prefetching, key-based state, and viewport calculation. For most use cases, the custom `Layout` approach with `Column` wrapping `Row` or a `FlowRow` with `maxItemsInEachRow` was a common workaround, but true masonry required manual column tracking. The key insight is that each item has a different intrinsic height, so you cannot use a simple row-based grid; you must maintain per-column height offsets.

```kotlin
@Composable
fun MasonryGrid(
    columns: Int,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        val columnWidth = constraints.maxWidth / columns
        val itemConstraints = Constraints(maxWidth = columnWidth)

        // Track the height of each column.
        val columnHeights = IntArray(columns) { 0 }
        val placeables = measurables.map { measurable ->
            val placeable = measurable.measure(itemConstraints)
            val shortestColumn = columnHeights.withIndex().minByOrNull { it.value }!!.index
            placeable to shortestColumn
        }

        // Recalculate final column heights for layout size.
        val finalHeights = IntArray(columns) { 0 }
        placeables.forEach { (placeable, column) ->
            finalHeights[column] += placeable.height
        }

        val maxHeight = finalHeights.maxOrNull()?.coerceIn(constraints.minHeight, constraints.maxHeight) ?: constraints.minHeight

        layout(constraints.maxWidth, maxHeight) {
            val currentColumnHeights = IntArray(columns) { 0 }
            placeables.forEach { (placeable, column) ->
                val x = column * columnWidth
                val y = currentColumnHeights[column]
                placeable.placeRelative(x, y)
                currentColumnHeights[column] += placeable.height
            }
        }
    }
}

// Usage
@Composable
fun PhotoMasonry(photos: List<Photo>) {
    MasonryGrid(columns = 2, modifier = Modifier.fillMaxSize().padding(8.dp)) {
        photos.forEach { photo ->
            AsyncImage(
                model = photo.url,
                contentDescription = null,
                modifier = Modifier.padding(4.dp),
                contentScale = ContentScale.Crop
            )
        }
    }
}
```

***

#### **Q27. How does Compose Navigation differ from the Fragment-based Navigation Component in terms of lifecycle and back stack?**

In the Fragment-based Navigation Component, each destination is a `Fragment` managed by the `FragmentManager`. This means each screen has its own independent lifecycle (`onCreate`, `onCreateView`, `onViewCreated`, `onDestroy`), its own `ViewModelStore` (by default), and the overhead of `FragmentTransaction` (view inflation, `Fragment` instance creation, and lifecycle dispatching). The back stack is a stack of `Fragment` transactions. In Compose Navigation, the entire navigation graph lives inside a single `NavHost` composable, typically hosted by one `Activity` or a single root `Fragment`. Destinations are **composables**, not Fragments. The lifecycle is managed through `NavBackStackEntry`, which exposes a `LifecycleOwner` and `ViewModelStore`. When you navigate, the `NavController` changes the current back stack entry, causing the `NavHost` to recompose and display the new destination. The old destination's composable leaves the Composition (triggering `DisposableEffect` cleanups) but there is no `Fragment` destruction overhead. Because there is only one `Activity`/`Fragment`, system insets and window handling are simpler, but you must be careful about `ViewModel` scoping: by default, `ViewModel`s are scoped to the `NavBackStackEntry`, so they are cleared when you pop the back stack, which is analogous to Fragment behavior but requires explicit scoping to a parent entry if you want to share them across destinations.

```kotlin
// Fragment-based: Each destination is a Fragment with its own lifecycle.
class DetailFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
            setContent { DetailScreen() }
        }
    }
}

// Compose Navigation: Single Activity, destinations are Composables.
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "home") {
        composable("home") { HomeScreen(onNavigate = { navController.navigate("detail/$it") }) }
        composable(
            "detail/{itemId}",
            arguments = listOf(navArgument("itemId") { type = NavType.StringType })
        ) { backStack ->
            val lifecycle by backStack.lifecycle.currentStateFlow.collectAsState()
            // Lifecycle is tied to NavBackStackEntry, not a Fragment.
            DetailScreen(
                itemId = backStack.arguments?.getString("itemId")!!,
                onBack = { navController.popBackStack() }
            )
        }
    }
}

// ViewModel scoping to NavBackStackEntry (automatic in Compose Navigation).
@Composable
fun DetailScreen(itemId: String) {
    val vm: DetailViewModel = viewModel(viewModelStoreOwner = LocalViewModelStoreOwner.current!!)
    // vm is cleared when this destination is popped from the back stack.
}
```

***

#### **Q28. How do you handle nested navigation graphs with independent back stacks in Compose?**

In a multi-tab app (e.g., Bottom Navigation), each tab should maintain its own navigation history so that switching tabs does not destroy the other tab's back stack. The standard pattern is to use a separate `NavHost` for each tab, each with its own `rememberNavController()`. The selected tab's `NavHost` is displayed while the others are hidden (or conditionally composed). You must persist the `NavController` instances across tab switches; since `rememberNavController` is tied to the Composition, you store the controllers in a `rememberSaveable` or simply recompose the same controller instance when the tab is reselected. Alternatively, you can use a single `NavHost` with nested navigation graphs (`navigation(startDestination = ..., route = "tab_a") { ... }`), but this shares one back stack, so popping from a nested graph pops to the start destination of that graph, not preserving independent histories. For true independent back stacks, the multi-`NavHost` approach is required. You also need to handle the system back button: intercept it with `BackHandler` and delegate to the currently selected tab's `NavController`.

```kotlin
@Composable
fun MainScreen() {
    val tabs = listOf("Home", "Search", "Profile")
    var selectedTab by rememberSaveable { mutableIntStateOf(0) }

    // Maintain a NavController per tab.
    val navControllers = remember {
        List(tabs.size) { NavHostController(null).apply { value = Bundle() } }
    }.map { rememberNavController() }

    Scaffold(
        bottomBar = {
            NavigationBar {
                tabs.forEachIndexed { index, title ->
                    NavigationBarItem(
                        selected = selectedTab == index,
                        onClick = { selectedTab = index },
                        icon = { Icon(Icons.Default.Home, contentDescription = title) },
                        label = { Text(title) }
                    )
                }
            }
        }
    ) { padding ->
        // Intercept system back for the active tab.
        BackHandler(enabled = true) {
            val currentController = navControllers[selectedTab]
            if (!currentController.popBackStack()) {
                // If back stack is empty, exit app or handle root back.
                (LocalContext.current as? Activity)?.finish()
            }
        }

        Box(modifier = Modifier.padding(padding)) {
            tabs.forEachIndexed { index, _ ->
                val isSelected = selectedTab == index
                // Only compose the active tab to keep its state alive.
                // Alternatively, use AnimatedVisibility to keep inactive tabs in composition.
                if (isSelected) {
                    TabNavHost(navController = navControllers[index], tabIndex = index)
                }
            }
        }
    }
}

@Composable
fun TabNavHost(navController: NavHostController, tabIndex: Int) {
    NavHost(navController = navController, startDestination = "tab_${tabIndex}_root") {
        composable("tab_${tabIndex}_root") { TabRootScreen(tabIndex, onDetail = { navController.navigate("detail/$it") }) }
        composable("detail/{id}") { DetailScreen(it.arguments?.getString("id")!!) }
    }
}
```

***

#### **Q29. What are the challenges of using type-safe navigation with Compose Navigation, and how do you implement it?**

Type-safe navigation in Compose Navigation (stable since version 2.8.0) uses Kotlin Serialization (`@Serializable` data classes) instead of string routes. The main challenges are: (1) **Deep linking**: You must manually register `NavDeepLink` for each route and ensure the URL path segments match the class structure; complex nested objects in routes are not automatically parsed from deep links. (2) **Custom types**: `Parcelable`, `Enum`, and complex objects require custom `NavType` registration in the `NavHostBuilder`, which reintroduces some boilerplate. (3) **Default arguments**: You cannot easily provide default values in the `@Serializable` class for navigation arguments that need to be optional in the graph; you must use nullable fields or separate route classes. (4) **Migration**: Existing string-based graphs require a full refactor. (5) **Library alignment**: The Kotlin Serialization plugin version must align with the Compose Navigation version. To implement it, you annotate your route objects with `@Serializable`, pass them directly to `composable<<YourRoute>()`, and extract arguments via `backStackEntry.toRoute()`. For custom types, define a `NavType` and register it in the `typeMap` parameter of `NavHost` or the `composable` builder.

```kotlin
// Define routes as serializable objects/classes.
@Serializable
object HomeRoute

@Serializable
data class DetailRoute(val itemId: String, val category: String? = null)

// Custom type for an enum or complex object.
enum class SortOrder { ASC, DESC }

class SortOrderType : NavType<SortOrder>(isNullableAllowed = false) {
    override fun get(bundle: Bundle, key: String) = bundle.getString(key)?.let { SortOrder.valueOf(it) }
    override fun parseValue(value: String) = SortOrder.valueOf(value)
    override fun put(bundle: Bundle, key: String, value: SortOrder) = bundle.putString(key, value.name)
}

@Composable
fun TypeSafeNavHost() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = HomeRoute,
        typeMap = mapOf(typeOf<SortOrder>() to SortOrderType()) // Register custom types.
    ) {
        composable<<HomeRoute> {
            HomeScreen(onItemClick = { id -> navController.navigate(DetailRoute(itemId = id, category = "movies")) })
        }
        composable<<DetailRoute> { backStack ->
            val route: DetailRoute = backStack.toRoute()
            DetailScreen(itemId = route.itemId, category = route.category)
        }
    }
}

// Deep link support with type-safe routes.
composable<<DetailRoute>(
    deepLinks = listOf(navDeepLink<DetailRoute>(basePath = "https://example.com/detail"))
) { ... }
```

***

#### **Q30. How do you manage screen transitions and shared element transitions between destinations in Compose Navigation?**

Compose Navigation supports enter/exit transitions per destination via the `enterTransition` and `exitTransition` parameters in the `composable` DSL. These use `AnimatedContentTransitionScope` and provide access to `slideInHorizontally`, `fadeIn`, `scaleIn`, etc. For shared element transitions (where a visual element morphs from one screen to another), you use the `SharedTransitionLayout` (available in `androidx.compose.animation:animation` 1.8.0+). This requires wrapping your `NavHost` (or the shared screens) in a `SharedTransitionLayout` and using `Modifier.sharedElement()` or `Modifier.sharedBounds()` with a matching `animatedVisibilityScope` key. The challenge with navigation is that the two composables are in different back stack entries and may not be in the Composition simultaneously during the transition. To solve this, you must use `AnimatedContent` or ensure the outgoing destination remains composed during the transition by using `AnimatedContentScope` provided by the `composable` builder. The `sharedElement` modifier needs the `AnimatedVisibilityScope` from the current `composable` destination to coordinate the animation with the framework.

```kotlin
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedElementNavHost() {
    val navController = rememberNavController()

    SharedTransitionLayout {
        NavHost(
            navController = navController,
            startDestination = "list",
            modifier = Modifier.fillMaxSize()
        ) {
            composable(
                "list",
                exitTransition = { fadeOut(animationSpec = tween(300)) }
            ) { backStack ->
                ListScreen(
                    animatedVisibilityScope = this@composable,
                    onItemClick = { imageRes ->
                        navController.navigate("detail/$imageRes")
                    }
                )
            }
            composable(
                "detail/{imageRes}",
                enterTransition = {
                    fadeIn(animationSpec = tween(300)) + slideInVertically { it / 2 }
                },
                exitTransition = { fadeOut(animationSpec = tween(300)) }
            ) { backStack ->
                val imageRes = backStack.arguments?.getString("imageRes")!!.toInt()
                DetailScreen(
                    imageRes = imageRes,
                    animatedVisibilityScope = this@composable
                )
            }
        }
    }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun ListScreen(
    animatedVisibilityScope: AnimatedVisibilityScope,
    onItemClick: (Int) -> Unit
) {
    LazyColumn {
        items(20) { index ->
            val resId = R.drawable.sample_image // placeholder logic
            Image(
                painter = painterResource(resId),
                contentDescription = null,
                modifier = Modifier
                    .size(80.dp)
                    .sharedElement(
                        state = rememberSharedContentState(key = "image-$index"),
                        animatedVisibilityScope = animatedVisibilityScope,
                        boundsTransform = { _, _ -> tween(500) }
                    )
                    .clickable { onItemClick(index) }
            )
        }
    }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun DetailScreen(imageRes: Int, animatedVisibilityScope: AnimatedVisibilityScope) {
    Image(
        painter = painterResource(R.drawable.sample_image),
        contentDescription = null,
        modifier = Modifier
            .fillMaxWidth()
            .aspectRatio(1f)
            .sharedElement(
                state = rememberSharedContentState(key = "image-$imageRes"),
                animatedVisibilityScope = animatedVisibilityScope,
                boundsTransform = { _, _ -> tween(500) }
            )
    )
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://notes.tejpratapsingh.com/_/android-tips/interview/compose/compose-part-3.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
