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:
| Component | Purpose |
|---|---|
NavController | Performs navigation and manages the back stack |
NavHost | Displays the current destination |
| Route | Identifies a destination |
| Navigation graph | Defines 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 HomeThe 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
└── Profileflowchart 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
* Notification
* Another application
* QR code
Example URL:
https://example.com/product/101Route:
@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"]| Requirement | API |
|---|---|
| Put Compose inside XML/View UI | ComposeView |
| Put a traditional View inside Compose | AndroidView |
| Inflate a View Binding layout in Compose | AndroidViewBinding |
---
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
animateOffsetAsStateval 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 --> HiddenAndroid: 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 = nullHeading
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.