Compose Navigation, Interoperability, Animations and Gestures

Compose Navigation, Interoperability, Animations and Gestures

Navigation and Interoperability

1. Navigation Compose

Navigation Compose manages movement between composable screens.

Main components:

ComponentPurpose
NavControllerPerforms navigation and manages the back stack
NavHostDisplays the current destination
RouteIdentifies a destination
Navigation graphDefines available destinations and connections
flowchart LR
    H["Home"] --> D["Details"]
    D --> P["Payment"]
    P --> S["Success"]

---

2. Basic Navigation

Create and remember a NavController:

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

    NavHost(
        navController = navController,
        startDestination = Home
    ) {
        composable<Home> {
            HomeScreen(
                productId ->
                    navController.navigate(
                        ProductDetails(productId)
                    )
                }
            )
        }

        composable<ProductDetails> { backStackEntry ->
            val route =
                backStackEntry.toRoute<ProductDetails>()

            ProductDetailsScreen(
                productId = route.productId,
               
            )
        }
    }
}

Define type-safe routes:

@Serializable
data object Home

@Serializable
data class ProductDetails(
    val productId: String
)

Type-safe routes reduce spelling mistakes and provide compile-time argument checking. Android: Type-safe Navigation Compose

---

3. Navigation Flow

sequenceDiagram
    actor U as User
    participant H as HomeScreen
    participant NC as NavController
    participant BS as Back Stack
    participant D as DetailsScreen

    U->>H: Select product
    H->>NC: navigate(ProductDetails)
    NC->>BS: Add destination
    NC->>D: Display details
    U->>D: Press Back
    NC->>BS: Remove details
    NC->>H: Display Home

The NavController adds destinations to and removes destinations from its back stack.

---

4. Routes and Arguments

Pass only the minimum information needed to identify the data.

Recommended

@Serializable
data class ProductDetails(
    val productId: String
)

Then load the product using the ID.

Avoid

Passing an entire large object:

ProductDetails(productObject)

Reasons:

* The object may become outdated.

* It may be too large.

* It may not be serializable.

* Restoring navigation becomes difficult.

Access argument in ViewModel

class ProductViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val route =
        savedStateHandle.toRoute<ProductDetails>()

    val productId = route.productId
}

---

5. Back-Stack Control

Navigate back

navController.navigateUp()

or:

navController.popBackStack()

Remove previous screens

For example, after successful login:
navController.navigate(Home) {
    popUpTo<Login> {
        inclusive = true
    }
}
flowchart LR
    L["Login"] --> H["Home"]
    H -. "Login removed" .-> X["Back cannot return to Login"]

Avoid duplicate destinations

navController.navigate(Home) {
    launchSingleTop = true
}

---

6. Nested Navigation Graphs

Nested graphs group related screens.

For example:

Main Graph
├── Authentication Graph
│   ├── Login
│   └── OTP
└── Home Graph
    ├── Home
    └── Profile
flowchart TD
    A["App Graph"] --> AU["Authentication Graph"]
    A --> MA["Main Graph"]
    AU --> L["Login"]
    AU --> O["OTP"]
    MA --> H["Home"]
    MA --> P["Profile"]

Example:

@Serializable
data object AuthGraph

@Serializable
data object MainGraph

NavHost(
    navController = navController,
    startDestination = AuthGraph
) {
    navigation<AuthGraph>(
        startDestination = Login
    ) {
        composable<Login> {
            LoginScreen()
        }

        composable<Otp> {
            OtpScreen()
        }
    }

    navigation<MainGraph>(
        startDestination = Home
    ) {
        composable<Home> {
            HomeScreen()
        }

        composable<Profile> {
            ProfileScreen()
        }
    }
}

Nested graphs are useful for:

* Login flow

* Onboarding flow

* Payment flow

* Feature-level organization

* Separate back-stack behaviour

---

7. Deep Links

A deep link opens a particular destination from:

* Website

* Email

* Notification

* Another application

* QR code

Example URL:

https://example.com/product/101

Route:

@Serializable
data class ProductDetails(
    val productId: String
)

Destination:

composable<ProductDetails>(
    deepLinks = listOf(
        navDeepLink<ProductDetails>(
            basePath = "https://example.com/product"
        )
    )
) { backStackEntry ->
    val route =
        backStackEntry.toRoute<ProductDetails>()

    ProductDetailsScreen(route.productId)
}
For an external URL to reach the Activity, a matching intent filter and app-link configuration are also required.

Deep-link flow

flowchart LR
    URL["Website or Notification"] --> OS["Android resolves link"]
    OS --> A["Application Activity"]
    A --> N["Navigation destination"]
    N --> S["Product screen"]

Always validate deep-link arguments because they come from an external source.

---

8. View Interoperability

Interoperability means using Compose and the traditional Android View system together.

Two directions are supported:

flowchart TD
    V["Existing View screen"] --> CV["ComposeView"]
    C["Compose screen"] --> AV["AndroidView"]
RequirementAPI
Put Compose inside XML/View UIComposeView
Put a traditional View inside ComposeAndroidView
Inflate a View Binding layout in ComposeAndroidViewBinding

---

9. ComposeView: Compose Inside Views

Use ComposeView to add composable UI to an existing Activity, Fragment or XML screen.

XML

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

Fragment

binding.composeView.apply {
    setViewCompositionStrategy(
        ViewCompositionStrategy
            .DisposeOnViewTreeLifecycleDestroyed
    )

    setContent {
        AppTheme {
            ProductCard()
        }
    }
}

The composition strategy ensures that Compose resources are disposed with the appropriate View lifecycle. Android: Using Compose in Views

---

10. AndroidView: Views Inside Compose

Use AndroidView for:

* Existing custom Views

* WebView

* MapView

* Advertisement SDK Views

* Third-party Views without Compose support

@Composable
fun LegacyCustomView(
    selectedItem: Int
) {
    AndroidView(
        factory = { context ->
            LegacyProductView(context).apply {
                // Create and configure once
            }
        },
        update = { view ->
            // Runs when observed Compose State changes
            view.selectedItem = selectedItem
        }
    )
}

Internal working

flowchart LR
    F["factory"] --> C["Create View once"]
    S["Compose State changes"] --> U["update"]
    U --> V["Update existing View"]

Important rules:

* Create the View inside factory.

* Do not recreate it during every recomposition.

* Use update to send Compose State to the View.

* Clean up listeners or resources when necessary.

Android: Using Views in Compose

---

11. Gradual Migration

A large View-based application does not need to migrate all screens together.

flowchart LR
    V1["Existing View app"] --> C1["Add one Compose component"]
    C1 --> C2["Convert one screen"]
    C2 --> C3["Convert feature screens"]
    C3 --> CA["Mostly Compose app"]

Recommended approach:

1. Keep the existing application working.
2. Add small Compose components using ComposeView.
3. Convert simple screens first.
4. Share existing ViewModels and State.
5. Replace legacy Views gradually.
6. Test each migrated screen.
7. Remove old XML only after migration is complete.

Avoid maintaining two independent sources of State for the same screen.

---

Animations, Gestures and Semantics

12. State-Based Animations

Compose animations are normally driven by State.

flowchart LR
    S1["Old State"] --> A["Animation"]
    A --> S2["Target State"]
    S2 --> UI["Updated UI"]

Example:

var expanded by remember {
    mutableStateOf(false)
}

val size by animateDpAsState(
    targetValue = if (expanded) 200.dp else 100.dp,
    label = "boxSize"
)

Box(
    modifier = Modifier
        .size(size)
        .background(Color.Blue)
        .clickable {
            expanded = !expanded
        }
)

When expanded changes, Compose animates from the current value to the target value.

---

13. animate*AsState

Use animate*AsState for one value.

Common APIs:

animateDpAsState
animateFloatAsState
animateColorAsState
animateIntAsState
animateOffsetAsState
val color by animateColorAsState(
    targetValue = if (selected) {
        Color.Green
    } else {
        Color.Gray
    },
    label = "selectionColor"
)

Use it when:

* One property changes

* The animation follows State

* Manual animation control is not required

Android: Value-based animations

---

14. Transitions

Use updateTransition when multiple values should animate together.

val transition = updateTransition(
    targetState = expanded,
    label = "cardTransition"
)

val height by transition.animateDp(
    label = "height"
) { isExpanded ->
    if (isExpanded) 200.dp else 80.dp
}

val color by transition.animateColor(
    label = "color"
) { isExpanded ->
    if (isExpanded) Color.Blue else Color.Gray
}
flowchart TD
    S["Expanded State"] --> T["Transition"]
    T --> H["Animate height"]
    T --> C["Animate color"]
    T --> A["Animate alpha"]

A transition keeps multiple animations synchronized.

---

15. AnimatedVisibility

Use AnimatedVisibility to animate content appearing and disappearing.

AnimatedVisibility(
    visible = showDetails,
    enter = fadeIn() + expandVertically(),
    exit = fadeOut() + shrinkVertically()
) {
    ProductDetails()
}

Unlike simply setting alpha to zero, AnimatedVisibility eventually removes hidden content from the composition.

stateDiagram-v2
    [*] --> Hidden
    Hidden --> Entering: visible = true
    Entering --> Visible
    Visible --> Exiting: visible = false
    Exiting --> Hidden

Android: Animation composables

---

16. Other Useful Animation APIs

| Requirement                     | API                            |
| ------------------------------- | ------------------------------ |
| Animate one value               | `animate*AsState`              |
| Animate multiple values         | `updateTransition`             |
| Show or hide content            | `AnimatedVisibility`           |
| Change one content into another | `AnimatedContent`              |
| Animate size changes            | `animateContentSize()`         |
| Manual gesture-driven animation | `Animatable`                   |
| Continuous animation            | `rememberInfiniteTransition()` |

Animation specifications

animationSpec = tween(
    durationMillis = 300
)

Common specifications:

* tween: Duration and easing based

* spring: Physics-based movement

* keyframes: Values at specific times

* repeatable: Repeats a fixed number

* infiniteRepeatable: Repeats continuously

---

17. High-Level Gestures

Prefer high-level gesture modifiers because they also provide accessibility, focus and interaction behaviour.

Modifier.clickable {
    onProductClick()
}

Common high-level APIs:

* clickable

* combinedClickable

* draggable

* scrollable

* transformable

* swipeable-style component APIs

flowchart TD
    G["User Gesture"] --> H["High-level modifier"]
    H --> A["Action callback"]
    H --> S["Semantics and interaction support"]

---

18. pointerInput

Use pointerInput when custom or low-level gesture handling is required.

Detect taps

Modifier.pointerInput(Unit) {
    detectTapGestures(
       
            onTap()
        },
       
            onLongPress()
        }
    )
}

Detect dragging

var offsetX by remember {
    mutableFloatStateOf(0f)
}

Modifier
    .offset {
        IntOffset(offsetX.roundToInt(), 0)
    }
    .pointerInput(Unit) {
        detectHorizontalDragGestures { change, amount ->
            change.consume()
            offsetX += amount
        }
    }

Pointer-input key

Modifier.pointerInput(enabled) {
    if (enabled) {
        detectTapGestures {
            onClick()
        }
    }
}

When enabled changes, the old pointer-input handler is cancelled and restarted.

Prefer clickable for normal clicks because raw pointer APIs do not automatically provide all accessibility and visual interaction behaviour. Android: Pointer input

---

19. Gesture Event Levels

flowchart TD
    H["High level"] --> C["clickable / scrollable"]
    C --> D["Gesture detectors"]
    D --> PI["pointerInput"]
    PI --> R["Raw pointer events"]

Use the highest-level API that meets the requirement:

1. Material component such as Button

2. High-level modifier such as clickable

3. Gesture detector such as detectDragGestures

4. Raw pointer events only for advanced behaviour

---

20. Nested Scrolling

Nested scrolling happens when a child and parent both participate in the same scroll gesture.

Examples:

* Collapsing toolbar with a scrolling list

* Bottom sheet containing a list

* Scrollable parent and scrollable child

flowchart TD
    G["Scroll gesture"] --> P["Parent pre-scroll"]
    P --> C["Child consumes scroll"]
    C --> PO["Parent post-scroll"]

The nested-scroll cycle normally includes:

1. Parent can consume part before the child.

2. Child consumes what it can.

3. Parent can consume remaining movement.

4. Similar processing occurs for fling velocity.

Custom behaviour uses NestedScrollConnection:

val connection = remember {
    object : NestedScrollConnection {
        override fun onPreScroll(
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            // Consume required scroll amount
            return Offset.Zero
        }
    }
}

Box(
    modifier = Modifier.nestedScroll(connection)
) {
    LazyColumn {
        // Content
    }
}

Do not manually create nested-scroll logic when Material components already support the required behaviour.

---

21. Semantics

Semantics describe the meaning and behaviour of UI elements.

They are used by:

* TalkBack

* Voice Access

* Switch Access

* Autofill

* Compose UI tests

flowchart LR
    UI["Visual UI"] --> ST["Semantics tree"]
    ST --> A["Accessibility services"]
    ST --> T["UI tests"]

Compose Material components already provide many semantics automatically. Custom components may require manual semantics. Android: Compose semantics

---

22. Common Semantics

Content description

Icon(
    imageVector = Icons.Default.Delete,
    contentDescription = "Delete product"
)

For a decorative icon:

contentDescription = null

Heading

Text(
    text = "Payment Details",
    modifier = Modifier.semantics {
        heading()
    }
)

State description

Modifier.semantics {
    stateDescription =
        if (selected) "Selected" else "Not selected"
}

Custom progress

Modifier.semantics {
    progressBarRangeInfo = ProgressBarRangeInfo(
        current = progress,
        range = 0f..1f
    )
}

---

23. Merging Semantics

Sometimes multiple visual elements should be treated as one accessible element.

Row(
    modifier = Modifier
        .semantics(mergeDescendants = true) {}
        .clickable {
            openProduct()
        }
) {
    ProductImage()
    ProductName()
    ProductPrice()
}
flowchart LR
    I["Image"] --> M["Merged product item"]
    N["Name"] --> M
    P["Price"] --> M
    M --> TB["One TalkBack focus"]

Material components such as Button commonly merge child semantics automatically.

---

24. Custom Interaction Behaviour

A custom gesture must also have an accessible alternative.

For example, a swipe-to-delete action may be difficult for some users. Add a custom accessibility action:
Modifier.semantics {
    customActions = listOf(
        CustomAccessibilityAction(
            label = "Delete product",
            action = {
                deleteProduct()
                true
            }
        )
    )
}

This allows accessibility services to expose the action without requiring the swipe gesture.

Rule

> If you create a custom touch interaction, also provide its meaning, current state and an accessible action.

---

Common Mistakes

Navigation and Interoperability

* Using string routes everywhere when type-safe routes are available

* Passing complete large objects as navigation arguments

* Forgetting to validate deep-link arguments

* Creating NavController repeatedly

* Navigating directly from deeply nested UI instead of sending a callback

* Forgetting back-stack cleanup after login or logout

* Recreating a View inside AndroidView.update

* Using an incorrect ComposeView disposal strategy

* Keeping separate View and Compose sources of truth

Animation, Gestures and Semantics

* Starting animations manually when State-based APIs are enough

* Using AnimatedVisibility for permanently invisible large UI unnecessarily

* Running expensive logic on every animation frame

* Using raw pointerInput for a normal button click

* Forgetting to consume pointer changes during custom gestures

* Using the wrong pointerInput key

* Adding custom gestures without accessibility alternatives

* Giving descriptions to decorative icons

* Forgetting semantics for custom-drawn controls

* Creating nested-scroll logic when an existing component already handles it

---

Interview Questions and Answers

Q1. What are the main parts of Navigation Compose?

NavController performs navigation, NavHost displays destinations, routes identify screens, and the navigation graph defines available destinations.

Q2. Why use type-safe routes?

They provide compile-time route and argument checking and reduce errors caused by manual string construction.

Q3. What should be passed as a navigation argument?

Pass the minimum identifier, such as a product ID. Load the full object from the application’s source of truth.

Q4. What is a nested navigation graph?

It groups related destinations such as login, onboarding or payment screens inside a parent graph.

Q5. What is a deep link?

A deep link opens a specific application destination using an external URI, notification or another application.

Q6. What is the difference between ComposeView and AndroidView?

ComposeView places Compose inside a View-based screen. AndroidView places a traditional Android View inside Compose.

Q7. How would you migrate a View app gradually?

Start with small Compose components using ComposeView, then migrate simple screens and features while keeping one source of State.

Q8. What is a State-based animation?

The application supplies a target State, and Compose automatically animates from the current value to the target.

Q9. When should we use updateTransition?

Use it when multiple properties such as size, colour and alpha must animate together based on one State.

Q10. Why prefer clickable over pointerInput?

clickable provides click handling along with accessibility semantics, focus, keyboard support and visual interaction feedback.

Q11. What is nested scrolling?

It is coordinated scrolling where a parent and child share and consume scroll movement or fling velocity.

Q12. What are semantics?

Semantics provide the meaning, state and actions of UI elements to accessibility services and UI testing tools.

Q13. Why are semantics important for custom gestures?

They provide an accessible alternative for users who cannot perform gestures such as swiping or dragging.

---

Final Interview Answer

> Navigation Compose uses a NavController, NavHost, routes and a navigation graph to manage destinations and the back stack. Type-safe routes provide safer arguments, nested graphs organize feature flows, and deep links open particular destinations from outside the application. During gradual migration, ComposeView puts Compose inside legacy View screens, while AndroidView puts existing Views inside Compose. Compose animations are generally driven by State using APIs such as animate*AsState, transitions and AnimatedVisibility. High-level gesture APIs should be preferred, while pointerInput is used for custom gestures. Nested scrolling coordinates parent and child scrolling, and semantics make custom UI and interactions accessible and testable.