AgentStack
SKILL verified MIT Self-run

Compose Navigation

skill-piyushverma0-android-agent-skills-compose-navigation · by piyushverma0

|

No reviews yet
0 installs
10 views
0.0% view→install

Install

$ agentstack add skill-piyushverma0-android-agent-skills-compose-navigation

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Compose Navigation? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Compose Navigation

Type-safe navigation prevents the most common navigation bugs in AI-built apps. These rules cover the complete navigation pattern from setup to --- name: compose-navigation description: | Jetpack Compose Navigation for Android AI agents — complete patterns. Use this skill for any navigation: NavHost, NavController, type-safe @Serializable routes, back stack, deep links, navigation arguments, composable(), nested graphs, shared ViewModels, navigateUp, navigateTopLevel, launchSingleTop, popUpTo, shared element transitions, predictive back gesture, transition animations between screens, bottom navigation, navigation with Hilt, passing arguments, result passing via SavedStateHandle, or any navigation pattern in Compose. Always apply before writing any NavHost or route. ---

Compose Navigation — Complete Guide

Type-safe navigation with shared element transitions and predictive back. These rules cover every pattern from simple routes to complex multi-graph apps.

Setup

[versions]
navigationCompose = "2.8.5"
[libraries]
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }

Rule 1: All routes are @Serializable — zero string routes

// navigation/Routes.kt — ALL routes defined here, nowhere else

@Serializable object HomeRoute
@Serializable object SearchRoute
@Serializable object InboxRoute
@Serializable object ProfileRoute
@Serializable object SettingsRoute

// Routes with required arguments
@Serializable data class ItemDetailRoute(val itemId: String)
@Serializable data class EditItemRoute(val itemId: String, val isNew: Boolean = false)
@Serializable data class UserProfileRoute(val userId: String, val fromNotification: Boolean = false)

// Graph containers
@Serializable object AuthGraph
@Serializable object LoginRoute
@Serializable object SignUpRoute
@Serializable object ForgotPasswordRoute

@Serializable object OnboardingGraph
@Serializable object OnboardingStep1Route
@Serializable object OnboardingStep2Route
@Serializable object OnboardingStep3Route

// ❌ NEVER use string routes
navController.navigate("detail/$itemId")           // typo-prone, no type safety
composable("detail/{itemId}") { backStackEntry ->
    backStackEntry.arguments?.getString("itemId")  // nullable, untyped
}

Rule 2: Complete NavHost — every destination registered

// navigation/AppNavHost.kt
@Composable
fun AppNavHost(
    navController: NavHostController = rememberNavController(),
    startDestination: Any = if (isLoggedIn) HomeRoute else AuthGraph
) {
    NavHost(
        navController    = navController,
        startDestination = startDestination,
        // M3 Expressive enter/exit transitions (applied to ALL screens by default)
        enterTransition  = { slideInHorizontally(
            initialOffsetX = { it },
            animationSpec  = tween(Duration.medium4, easing = AppEasing.EmphasizedDecel)
        ) + fadeIn(tween(Duration.medium4)) },
        exitTransition   = { slideOutHorizontally(
            targetOffsetX = { -it / 4 },
            animationSpec = tween(Duration.short4, easing = AppEasing.EmphasizedAccel)
        ) + fadeOut(tween(Duration.short4)) },
        popEnterTransition = { slideInHorizontally(
            initialOffsetX = { -it / 4 },
            animationSpec  = tween(Duration.medium4, easing = AppEasing.EmphasizedDecel)
        ) + fadeIn(tween(Duration.medium4)) },
        popExitTransition  = { slideOutHorizontally(
            targetOffsetX = { it },
            animationSpec = tween(Duration.short4, easing = AppEasing.EmphasizedAccel)
        ) + fadeOut(tween(Duration.short4)) }
    ) {
        // Top-level destinations
        composable   { HomeScreen(navController = navController) }
        composable { SearchScreen(navController = navController) }
        composable  { InboxScreen(navController = navController) }
        composable{ ProfileScreen(navController = navController) }
        composable{ SettingsScreen(onBack = { navController.navigateUp() }) }

        // Detail screens
        composable { entry ->
            val route: ItemDetailRoute = entry.toRoute()
            ItemDetailScreen(
                itemId  = route.itemId,
                onEdit  = { navController.navigate(EditItemRoute(route.itemId)) },
                onBack  = { navController.navigateUp() }
            )
        }
        composable { entry ->
            val route: EditItemRoute = entry.toRoute()
            EditItemScreen(
                itemId = route.itemId,
                isNew  = route.isNew,
                onDone = { navController.navigateUp() }
            )
        }

        // Auth flow — nested graph
        navigation(startDestination = LoginRoute) {
            composable {
                LoginScreen(
                    onLoginSuccess = {
                        navController.navigate(HomeRoute) {
                            popUpTo(AuthGraph) { inclusive = true }  // clear auth stack entirely
                        }
                    },
                    onSignUp       = { navController.navigate(SignUpRoute) },
                    onForgotPass   = { navController.navigate(ForgotPasswordRoute) }
                )
            }
            composable {
                SignUpScreen(
                    onSuccess = { navController.navigate(HomeRoute) { popUpTo(AuthGraph) { inclusive = true } } },
                    onBack    = { navController.navigateUp() }
                )
            }
            composable {
                ForgotPasswordScreen(onBack = { navController.navigateUp() })
            }
        }
    }
}

Rule 3: Top-level navigation — launchSingleTop prevents duplicate entries

// ✅ Navigation helper for tab/nav items — prevents backstack pollution
fun NavController.navigateTopLevel(route: Any) {
    navigate(route) {
        // Pop to start destination, saving state for restoration
        popUpTo(graph.findStartDestination().id) {
            saveState = true
        }
        launchSingleTop = true  // don't create duplicate on re-tap
        restoreState = true     // restore state when re-navigating to tab
    }
}

// Usage in NavigationSuiteScaffold
item(
    selected = currentDestination?.hasRoute() == true,
    onClick  = { navController.navigateTopLevel(HomeRoute) }
)

Rule 4: Shared Element Transitions — M3 Expressive

// ✅ Shared element transition between list item and detail screen
// Navigation 2.8+ with material3 1.4+ supports SharedTransitionLayout

@Composable
fun HomeScreen(navController: NavController) {
    SharedTransitionLayout {
        AnimatedContent(targetState = selectedItem, label = "shared-transition") { item ->
            if (item == null) {
                LazyColumn {
                    items(items, key = { it.id }) { item ->
                        ItemCard(
                            item = item,
                            animatedVisibilityScope = this@AnimatedContent,
                            sharedTransitionScope = this@SharedTransitionLayout,
                            onClick = { selectedItem = item }
                        )
                    }
                }
            } else {
                ItemDetailScreen(
                    item = item,
                    animatedVisibilityScope = this@AnimatedContent,
                    sharedTransitionScope = this@SharedTransitionLayout,
                    onBack = { selectedItem = null }
                )
            }
        }
    }
}

// ✅ In list item — mark shared elements
@Composable
fun ItemCard(
    item: Item,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope,
    onClick: () -> Unit,
) {
    with(sharedTransitionScope) {
        Card(onClick = onClick) {
            AsyncImage(
                model = item.imageUrl,
                contentDescription = null,
                modifier = Modifier
                    .sharedElement(
                        state = rememberSharedContentState("image-${item.id}"),
                        animatedVisibilityScope = animatedVisibilityScope
                    )
                    .fillMaxWidth().aspectRatio(16f / 9f)
            )
            Text(
                item.title,
                modifier = Modifier.sharedElement(
                    state = rememberSharedContentState("title-${item.id}"),
                    animatedVisibilityScope = animatedVisibilityScope
                )
            )
        }
    }
}

// ✅ In detail screen — same shared element keys
@Composable
fun ItemDetailScreen(
    item: Item,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope,
    onBack: () -> Unit
) {
    with(sharedTransitionScope) {
        Column {
            AsyncImage(
                model = item.imageUrl,
                contentDescription = item.title,
                modifier = Modifier
                    .sharedElement(
                        state = rememberSharedContentState("image-${item.id}"),
                        animatedVisibilityScope = animatedVisibilityScope
                    )
                    .fillMaxWidth().aspectRatio(16f / 9f)
            )
            Text(
                item.title,
                style = MaterialTheme.typography.headlineMedium,
                modifier = Modifier.sharedElement(
                    state = rememberSharedContentState("title-${item.id}"),
                    animatedVisibilityScope = animatedVisibilityScope
                ).padding(Spacing.md)
            )
        }
    }
}

Rule 5: Deep links with type-safe routes

// ✅ Deep link registered on destination
composable(
    deepLinks = listOf(
        navDeepLink(basePath = "https://myapp.com/items")
        // matches: https://myapp.com/items/{itemId}
    )
) { entry ->
    val route: ItemDetailRoute = entry.toRoute()
    ItemDetailScreen(itemId = route.itemId)
}

// AndroidManifest.xml — intent filter
// 
//     
//         
//         
//         
//         
//     
// 

Rule 6: Shared ViewModel scoped to NavGraph

// ✅ Share state across multiple screens in a graph
navigation(startDestination = CartRoute) {
    composable { entry ->
        val parentEntry = remember(entry) { navController.getBackStackEntry(CheckoutGraph) }
        val viewModel: CheckoutViewModel = hiltViewModel(parentEntry)
        CartScreen(viewModel)
    }
    composable { entry ->
        val parentEntry = remember(entry) { navController.getBackStackEntry(CheckoutGraph) }
        val viewModel: CheckoutViewModel = hiltViewModel(parentEntry)
        ShippingScreen(viewModel)
    }
    composable { entry ->
        val parentEntry = remember(entry) { navController.getBackStackEntry(CheckoutGraph) }
        val viewModel: CheckoutViewModel = hiltViewModel(parentEntry)
        PaymentScreen(viewModel)
    }
}

Rule 7: Result passing via SavedStateHandle

// ✅ Pass result back to previous destination
// In destination that produces result:
@Composable
fun CreateItemScreen(navController: NavController) {
    val onSaved = { newItemId: String ->
        navController.previousBackStackEntry
            ?.savedStateHandle
            ?.set("new_item_id", newItemId)
        navController.navigateUp()
    }
}

// In destination that receives result:
@HiltViewModel
class HomeViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    val newItemId = savedStateHandle.getStateFlow("new_item_id", null)
}

Rule 8: Predictive back — enable in manifest


// ✅ NavHost 2.8+ handles predictive back automatically for type-safe routes
// ✅ Custom back handler with animation for non-NavHost back
BackHandler {
    coroutineScope.launch {
        // Animate out before popping
        isVisible = false
        delay(Duration.short4.toLong())
        navController.navigateUp()
    }
}

Common Mistakes

❌ String routes anywhere — use @Serializable object/data class ❌ navController.navigate(route) for tabs — use navigateTopLevel()navController.navigate(SomeRoute) { popUpTo(0) } — pops past graph root; use graph.findStartDestination().idhiltViewModel() in every screen that shares state — scope to NavGraph entry ❌ Missing popUpTo(AuthGraph) { inclusive = true } after login — user can press Back to login ❌ Passing complex objects as route args — pass only IDs, fetch in ViewModel ❌ SharedElement keys not matching between list and detail — transition won't animate ❌ No screen-level transitions in NavHost — add enter/exit/pop specs at NavHost leveldeep links.

Setup

# libs.versions.toml
navigationCompose = "2.8.3"
[libraries]
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }

Rule 1: Type-safe routes with @Serializable

// ✅ Define all routes as @Serializable objects/data classes
// navigation/Routes.kt

@Serializable
object HomeRoute

@Serializable
object SearchRoute

@Serializable
data class ItemDetailRoute(val itemId: String)

@Serializable
data class EditItemRoute(val itemId: String, val isNew: Boolean = false)

@Serializable
object SettingsRoute

// ❌ String routes — typos compile, arguments are untyped
navController.navigate("detail/$itemId")          // typo-prone
composable("detail/{itemId}") { backStackEntry ->
    val id = backStackEntry.arguments?.getString("itemId")  // nullable, untyped
}

Rule 2: NavHost setup

// ✅ Complete NavHost with type-safe destinations
@Composable
fun AppNavHost(
    navController: NavHostController = rememberNavController(),
    startDestination: Any = HomeRoute
) {
    NavHost(
        navController = navController,
        startDestination = startDestination
    ) {
        composable {
            HomeScreen(
                onItemClick = { itemId ->
                    navController.navigate(ItemDetailRoute(itemId))
                },
                onSearchClick = { navController.navigate(SearchRoute) }
            )
        }

        composable {
            SearchScreen(
                onItemClick = { itemId ->
                    navController.navigate(ItemDetailRoute(itemId))
                },
                onBackClick = { navController.navigateUp() }
            )
        }

        composable { backStackEntry ->
            val route: ItemDetailRoute = backStackEntry.toRoute()
            ItemDetailScreen(
                itemId = route.itemId,
                onEditClick = { navController.navigate(EditItemRoute(route.itemId)) },
                onBackClick = { navController.navigateUp() }
            )
        }

        composable { backStackEntry ->
            val route: EditItemRoute = backStackEntry.toRoute()
            EditItemScreen(
                itemId = route.itemId,
                isNew = route.isNew,
                onSaved = { navController.navigateUp() },
                onCancel = { navController.navigateUp() }
            )
        }

        composable {
            SettingsScreen(onBackClick = { navController.navigateUp() })
        }
    }
}

Rule 3: Navigate with launchSingleTop for tabs

// ✅ Tab navigation — prevent duplicate destinations on backstack
fun NavController.navigateToTopLevel(route: Any) {
    navigate(route) {
        popUpTo(graph.findStart

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [piyushverma0](https://github.com/piyushverma0)
- **Source:** [piyushverma0/android-agent-skills](https://github.com/piyushverma0/android-agent-skills)
- **License:** MIT
- **Homepage:** https://android-agent-skills.vercel.app

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.