Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
ahmed3elshaer avatar

Navigation Compose

  • 19 installs
  • 60 repo stars
  • Updated June 14, 2026
  • ahmed3elshaer/everything-claude-code-mobile

navigation-compose is a Claude Code skill that provides Jetpack Compose Navigation patterns including type-safe routes, NavHost setup, argument passing, deep links, and nested graphs.

About

navigation-compose is a Claude Code skill that documents Jetpack Compose Navigation patterns. It covers type-safe routes with serializable sealed interfaces, NavHost configuration, argument and result passing, deep link registration, nested graphs, and bottom navigation. Developers use it when wiring navigation between Compose screens in an Android app. Both the type-safe and legacy navArgument approaches are shown.

  • Type-safe Jetpack Compose Navigation with @Serializable sealed routes
  • NavHost setup, argument passing, and result passing via SavedStateHandle
  • Deep link registration for HTTPS and custom URI schemes

Navigation Compose by the numbers

  • 19 all-time installs (skills.sh)
  • Ranked #757 of 1,039 Mobile Development skills by installs in the Skillselion catalog
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
At a glance

navigation-compose capabilities & compatibility

Capabilities
navigation · deep linking · routing
Use cases
frontend
From the docs

What navigation-compose says it does

# Jetpack Compose Navigation Patterns
SKILL.md
For non-serializable routes, use the classic approach:
SKILL.md
npx skills add https://github.com/ahmed3elshaer/everything-claude-code-mobile --skill navigation-compose

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs19
repo stars60
Last updatedJune 14, 2026
Repositoryahmed3elshaer/everything-claude-code-mobile

What it does

Wire Jetpack Compose Navigation with type-safe routes, argument and result passing, deep links, and nested graphs.

Who is it for?

Setting up navigation between Jetpack Compose screens with type-safe routes and deep links

Skip if: iOS/SwiftUI navigation or non-Compose Android navigation

When should I use this skill?

Adding navigation, deep links, or argument passing between Compose screens

What you get

Screens navigate via serializable sealed routes with typed arguments, results, and registered deep links

By the numbers

  • Covers type-safe, legacy navArgument, result, and deep-link navigation approaches

Files

SKILL.mdMarkdownGitHub ↗

Jetpack Compose Navigation Patterns

Dependencies

dependencies {
    implementation("androidx.navigation:navigation-compose:2.8.5")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}

Type-Safe Routes with Sealed Interface

@Serializable
sealed interface Route {
    @Serializable
    data object Home : Route

    @Serializable
    data object Settings : Route

    @Serializable
    data class UserProfile(val userId: String) : Route

    @Serializable
    data class PostDetail(val postId: Long, val showComments: Boolean = false) : Route
}

// For nested graphs
@Serializable
sealed interface AuthGraph {
    @Serializable
    data object Login : AuthGraph

    @Serializable
    data object Register : AuthGraph

    @Serializable
    data object ForgotPassword : AuthGraph
}

NavHost Configuration

@Composable
fun AppNavHost(
    navController: NavHostController = rememberNavController(),
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Route.Home,
        modifier = modifier
    ) {
        composable<Route.Home> {
            HomeScreen(
                onNavigateToProfile = { userId ->
                    navController.navigate(Route.UserProfile(userId))
                },
                onNavigateToSettings = {
                    navController.navigate(Route.Settings)
                }
            )
        }

        composable<Route.Settings> {
            SettingsScreen(onBack = { navController.popBackStack() })
        }

        composable<Route.UserProfile> { backStackEntry ->
            val route = backStackEntry.toRoute<Route.UserProfile>()
            UserProfileScreen(userId = route.userId)
        }

        composable<Route.PostDetail> { backStackEntry ->
            val route = backStackEntry.toRoute<Route.PostDetail>()
            PostDetailScreen(
                postId = route.postId,
                showComments = route.showComments
            )
        }
    }
}

Argument Passing with Legacy navArgument

For non-serializable routes, use the classic approach:

composable(
    route = "post/{postId}?showComments={showComments}",
    arguments = listOf(
        navArgument("postId") { type = NavType.LongType },
        navArgument("showComments") {
            type = NavType.BoolType
            defaultValue = false
        }
    )
) { backStackEntry ->
    val postId = backStackEntry.arguments?.getLong("postId") ?: return@composable
    val showComments = backStackEntry.arguments?.getBoolean("showComments") ?: false
    PostDetailScreen(postId = postId, showComments = showComments)
}

// Navigate
navController.navigate("post/$postId?showComments=true")

Navigation with Results (SavedStateHandle)

// Screen A: Navigate and listen for result
@Composable
fun ScreenA(navController: NavHostController) {
    val result = navController.currentBackStackEntry
        ?.savedStateHandle
        ?.getStateFlow<String?>("selected_item", null)
        ?.collectAsState()

    LaunchedEffect(result?.value) {
        result?.value?.let { item ->
            // Handle the result
        }
    }

    Button(onClick = { navController.navigate(Route.ItemPicker) }) {
        Text("Pick Item")
    }
}

// Screen B: Set result and go back
@Composable
fun ItemPickerScreen(navController: NavHostController) {
    Button(onClick = {
        navController.previousBackStackEntry
            ?.savedStateHandle
            ?.set("selected_item", "chosen_value")
        navController.popBackStack()
    }) {
        Text("Select This")
    }
}

Deep Link Registration

composable<Route.PostDetail>(
    deepLinks = listOf(
        navDeepLink {
            uriPattern = "https://example.com/posts/{postId}"
        },
        navDeepLink {
            uriPattern = "myapp://posts/{postId}"
        }
    )
) { backStackEntry ->
    val route = backStackEntry.toRoute<Route.PostDetail>()
    PostDetailScreen(postId = route.postId)
}

AndroidManifest.xml intent filter:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="example.com" />
        <data android:scheme="myapp" />
    </intent-filter>
</activity>

Nested Navigation Graphs

fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
    navigation<AuthGraph.Login>(startDestination = AuthGraph.Login) {
        composable<AuthGraph.Login> {
            LoginScreen(
                onLoginSuccess = {
                    navController.navigate(Route.Home) {
                        popUpTo(AuthGraph.Login) { inclusive = true }
                    }
                },
                onNavigateToRegister = {
                    navController.navigate(AuthGraph.Register)
                }
            )
        }
        composable<AuthGraph.Register> {
            RegisterScreen(onBack = { navController.popBackStack() })
        }
        composable<AuthGraph.ForgotPassword> {
            ForgotPasswordScreen(onBack = { navController.popBackStack() })
        }
    }
}

// In the main NavHost
NavHost(navController = navController, startDestination = AuthGraph.Login) {
    authNavGraph(navController)
    composable<Route.Home> { HomeScreen() }
}

Bottom Navigation

@Serializable
sealed interface BottomTab {
    @Serializable data object Feed : BottomTab
    @Serializable data object Search : BottomTab
    @Serializable data object Profile : BottomTab
}

data class BottomNavItem(
    val route: BottomTab,
    val label: String,
    val icon: ImageVector
)

val bottomNavItems = listOf(
    BottomNavItem(BottomTab.Feed, "Feed", Icons.Default.Home),
    BottomNavItem(BottomTab.Search, "Search", Icons.Default.Search),
    BottomNavItem(BottomTab.Profile, "Profile", Icons.Default.Person)
)

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

    Scaffold(
        bottomBar = {
            NavigationBar {
                bottomNavItems.forEach { item ->
                    val isSelected = navBackStackEntry?.destination?.hasRoute(
                        item.route::class
                    ) == true

                    NavigationBarItem(
                        selected = isSelected,
                        onClick = {
                            navController.navigate(item.route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        },
                        icon = { Icon(item.icon, contentDescription = item.label) },
                        label = { Text(item.label) }
                    )
                }
            }
        }
    ) { padding ->
        NavHost(
            navController = navController,
            startDestination = BottomTab.Feed,
            modifier = Modifier.padding(padding)
        ) {
            composable<BottomTab.Feed> { FeedScreen() }
            composable<BottomTab.Search> { SearchScreen() }
            composable<BottomTab.Profile> { ProfileScreen() }
        }
    }
}

Animated Transitions

composable<Route.PostDetail>(
    enterTransition = {
        slideIntoContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Left,
            animationSpec = tween(300)
        )
    },
    exitTransition = {
        slideOutOfContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Left,
            animationSpec = tween(300)
        )
    },
    popEnterTransition = {
        slideIntoContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Right,
            animationSpec = tween(300)
        )
    },
    popExitTransition = {
        slideOutOfContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Right,
            animationSpec = tween(300)
        )
    }
) { backStackEntry ->
    val route = backStackEntry.toRoute<Route.PostDetail>()
    PostDetailScreen(postId = route.postId)
}

Navigation Testing

@Test
fun navigateToProfile_displaysUserProfile() {
    val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

    composeTestRule.setContent {
        navController.navigatorProvider.addNavigator(ComposeNavigator())
        AppNavHost(navController = navController)
    }

    composeTestRule.onNodeWithText("View Profile").performClick()

    val currentRoute = navController.currentBackStackEntry?.destination?.route
    assertTrue(currentRoute?.contains("UserProfile") == true)
}

Best Practices

  • Use type-safe routes with @Serializable data classes/objects over raw string routes.
  • Keep navigation logic out of composables; pass lambda callbacks (onNavigateTo) instead.
  • Use popUpTo with inclusive = true when navigating after login to clear the auth stack.
  • Use launchSingleTop = true for bottom tabs to prevent duplicate destinations.
  • Save and restore tab state with saveState = true and restoreState = true.
  • Scope ViewModels to navigation entries with hiltViewModel() or koinViewModel().
  • Test navigation by asserting on navController.currentBackStackEntry.

Related skills

FAQ

How are type-safe routes defined?

As @Serializable sealed interfaces whose data objects and data classes become composable<Route.X> destinations.

How do you return a result to a previous screen?

By writing to the previous back stack entry's SavedStateHandle and calling popBackStack().

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.