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

Compose Navigation

  • 446 installs
  • 910 repo stars
  • Updated July 27, 2026
  • new-silvermoon/awesome-android-agent-skills

compose-navigation is a Claude Code skill that implements type-safe Jetpack Compose navigation with NavHost, routes, arguments, deep links, and nested graphs so developers who build Android apps can keep multi-screen flo

About

compose-navigation is a Claude Code skill from new-silvermoon/awesome-android-agent-skills for Jetpack Compose apps using Navigation Compose. It documents adding androidx.navigation:navigation-compose:2.8.5 plus type-safe navigation dependencies, then walks through NavHost setup, argument passing between screens, deep link handling, nested graphs, adaptive navigation, and testing strategies. Developers reach for compose-navigation when scaffolding a new multi-screen Compose app, wiring route parameters, or replacing fragile string-based routes with typed destinations. The skill targets maintainable user flows—back stack behavior, graph structure, and test hooks—rather than one-off screen composables alone. It assumes Kotlin Gradle projects and Navigation Compose rather than legacy XML Navigation or fragment transactions.

  • NavHost and NavController setup
  • Type-safe route definitions
  • Back stack and pop behavior
  • Deep link argument handling

Compose Navigation by the numbers

  • 446 all-time installs (skills.sh)
  • +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
  • Ranked #318 of 1,039 Mobile Development skills by installs in the Skillselion catalog
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/new-silvermoon/awesome-android-agent-skills --skill compose-navigation

Add your badge

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

Listed on Skillselion
Installs446
repo stars910
Last updatedJuly 27, 2026
Repositorynew-silvermoon/awesome-android-agent-skills

How do you set up Compose Navigation with deep links?

Define type-safe navigation graphs, routes, arguments, and deep links in Jetpack Compose so multi-screen Android user flows stay maintainable and testable.

Who is it for?

Android developers building multi-screen Jetpack Compose apps who need type-safe Navigation Compose graphs, deep links, and testable routes.

Skip if: Legacy XML Navigation or single-screen Compose apps with no inter-screen routing requirements.

When should I use this skill?

A developer asks to set up Compose navigation, pass screen arguments, handle deep links, or structure multi-screen Android apps with NavHost.

What you get

NavHost graph, typed routes and arguments, deep link mappings, nested navigation graphs, and Compose navigation tests.

  • Navigation graph
  • Deep link config
  • Compose navigation tests

By the numbers

  • Documents androidx.navigation:navigation-compose:2.8.5 dependency
  • Covers six areas: NavHost, arguments, deep links, nested graphs, adaptive navigation, testing

Files

SKILL.mdMarkdownGitHub ↗

Compose Navigation

Overview

Implement type-safe navigation in Jetpack Compose applications using the Navigation Compose library. This skill covers NavHost setup, argument passing, deep links, nested graphs, adaptive navigation, and testing.

Setup

Add the Navigation Compose dependency:

// build.gradle.kts
dependencies {
    implementation("androidx.navigation:navigation-compose:2.8.5")
    
    // For type-safe navigation (recommended)
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}

// Enable serialization plugin
plugins {
    kotlin("plugin.serialization") version "2.0.21"
}

---

Core Concepts

1. Define Routes (Type-Safe)

Use @Serializable data classes/objects for type-safe routes:

import kotlinx.serialization.Serializable

// Simple screen (no arguments)
@Serializable
object Home

// Screen with required argument
@Serializable
data class Profile(val userId: String)

// Screen with optional argument
@Serializable
data class Settings(val section: String? = null)

// Screen with multiple arguments
@Serializable
data class ProductDetail(val productId: String, val showReviews: Boolean = false)

2. Create NavController

@Composable
fun MyApp() {
    val navController = rememberNavController()
    
    AppNavHost(navController = navController)
}

3. Create NavHost

@Composable
fun AppNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Home,
        modifier = modifier
    ) {
        composable<Home> {
            HomeScreen(
                onNavigateToProfile = { userId ->
                    navController.navigate(Profile(userId))
                }
            )
        }
        
        composable<Profile> { backStackEntry ->
            val profile: Profile = backStackEntry.toRoute()
            ProfileScreen(userId = profile.userId)
        }
        
        composable<Settings> { backStackEntry ->
            val settings: Settings = backStackEntry.toRoute()
            SettingsScreen(section = settings.section)
        }
    }
}

---

Navigation Patterns

Basic Navigation

// Navigate forward
navController.navigate(Profile(userId = "user123"))

// Navigate and pop current screen
navController.navigate(Home) {
    popUpTo<Home> { inclusive = true }
}

// Navigate back
navController.popBackStack()

// Navigate back to specific destination
navController.popBackStack<Home>(inclusive = false)

Navigate with Options

navController.navigate(Profile(userId = "user123")) {
    // Pop up to destination (clear back stack)
    popUpTo<Home> {
        inclusive = false  // Keep Home in stack
        saveState = true   // Save state of popped screens
    }
    
    // Avoid multiple copies of same destination
    launchSingleTop = true
    
    // Restore state when navigating to this destination
    restoreState = true
}

Bottom Navigation Pattern

@Composable
fun MainScreen() {
    val navController = rememberNavController()
    
    Scaffold(
        bottomBar = {
            NavigationBar {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentDestination = navBackStackEntry?.destination
                
                NavigationBarItem(
                    icon = { Icon(Icons.Default.Home, contentDescription = "Home") },
                    label = { Text("Home") },
                    selected = currentDestination?.hasRoute<Home>() == true,
                    onClick = {
                        navController.navigate(Home) {
                            popUpTo(navController.graph.findStartDestination().id) {
                                saveState = true
                            }
                            launchSingleTop = true
                            restoreState = true
                        }
                    }
                )
                // Add more items...
            }
        }
    ) { innerPadding ->
        AppNavHost(
            navController = navController,
            modifier = Modifier.padding(innerPadding)
        )
    }
}

---

Argument Handling

Retrieve Arguments in Composable

composable<Profile> { backStackEntry ->
    val profile: Profile = backStackEntry.toRoute()
    ProfileScreen(userId = profile.userId)
}

Retrieve Arguments in ViewModel

@HiltViewModel
class ProfileViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val userRepository: UserRepository
) : ViewModel() {
    
    private val profile: Profile = savedStateHandle.toRoute<Profile>()
    
    val user: StateFlow<User?> = userRepository
        .getUser(profile.userId)
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
}

Complex Data: Pass IDs, Not Objects

// CORRECT: Pass only the ID
navController.navigate(Profile(userId = "user123"))

// In ViewModel, fetch from repository
class ProfileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private val profile = savedStateHandle.toRoute<Profile>()
    val user = userRepository.getUser(profile.userId)
}

// INCORRECT: Don't pass complex objects
// navController.navigate(Profile(user = complexUserObject)) // BAD!

---

Deep Links

Define Deep Links

@Serializable
data class Profile(val userId: String)

composable<Profile>(
    deepLinks = listOf(
        navDeepLink<Profile>(basePath = "https://example.com/profile")
    )
) { backStackEntry ->
    val profile: Profile = backStackEntry.toRoute()
    ProfileScreen(userId = profile.userId)
}

Manifest Configuration

<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" />
    </intent-filter>
</activity>

Create PendingIntent for Notifications

val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://example.com/profile/user123".toUri(),
    context,
    MainActivity::class.java
)

val pendingIntent = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}

---

Nested Navigation

Create Nested Graph

NavHost(navController = navController, startDestination = Home) {
    composable<Home> { HomeScreen() }
    
    // Nested graph for authentication flow
    navigation<AuthGraph>(startDestination = Login) {
        composable<Login> {
            LoginScreen(
                onLoginSuccess = {
                    navController.navigate(Home) {
                        popUpTo<AuthGraph> { inclusive = true }
                    }
                }
            )
        }
        composable<Register> { RegisterScreen() }
        composable<ForgotPassword> { ForgotPasswordScreen() }
    }
}

// Route definitions
@Serializable object AuthGraph
@Serializable object Login
@Serializable object Register
@Serializable object ForgotPassword

---

Adaptive Navigation

Use NavigationSuiteScaffold for responsive navigation (bottom bar on phones, rail on tablets):

@Composable
fun AdaptiveApp() {
    val navController = rememberNavController()
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination
    
    NavigationSuiteScaffold(
        navigationSuiteItems = {
            item(
                icon = { Icon(Icons.Default.Home, contentDescription = "Home") },
                label = { Text("Home") },
                selected = currentDestination?.hasRoute<Home>() == true,
                onClick = { navController.navigate(Home) }
            )
            item(
                icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") },
                label = { Text("Settings") },
                selected = currentDestination?.hasRoute<Settings>() == true,
                onClick = { navController.navigate(Settings()) }
            )
        }
    ) {
        AppNavHost(navController = navController)
    }
}

---

Testing

Setup

// build.gradle.kts
androidTestImplementation("androidx.navigation:navigation-testing:2.8.5")

Test Navigation

class NavigationTest {
    @get:Rule
    val composeTestRule = createComposeRule()
    
    private lateinit var navController: TestNavHostController
    
    @Before
    fun setup() {
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            AppNavHost(navController = navController)
        }
    }
    
    @Test
    fun verifyStartDestination() {
        composeTestRule
            .onNodeWithText("Welcome")
            .assertIsDisplayed()
    }
    
    @Test
    fun navigateToProfile_displaysProfileScreen() {
        composeTestRule
            .onNodeWithText("View Profile")
            .performClick()
        
        assertTrue(
            navController.currentBackStackEntry?.destination?.hasRoute<Profile>() == true
        )
    }
}

---

Critical Rules

DO

  • Use @Serializable routes for type safety
  • Pass only IDs/primitives as arguments
  • Use popUpTo with launchSingleTop for bottom navigation
  • Extract NavHost to a separate composable for testability
  • Use SavedStateHandle.toRoute<T>() in ViewModels

DON'T

  • Pass complex objects as navigation arguments
  • Create NavController inside NavHost
  • Navigate in LaunchedEffect without proper keys
  • Forget FLAG_IMMUTABLE for PendingIntents (Android 12+)
  • Use string-based routes (legacy pattern)

---

References

Related skills

How it compares

Use compose-navigation for Navigation Compose graphs; use a general Compose UI skill when building individual screens without routing.

FAQ

Which Navigation Compose version does compose-navigation use?

compose-navigation documents androidx.navigation:navigation-compose:2.8.5 in build.gradle.kts plus type-safe navigation dependencies. The skill covers NavHost setup, arguments, deep links, nested graphs, adaptive navigation, and tests.

What does compose-navigation cover beyond basic routes?

compose-navigation covers argument passing, deep links, nested graphs, adaptive navigation patterns, and navigation testing—not only a minimal NavHost. The goal is maintainable multi-screen Compose user flows.

This week in AI coding

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

unsubscribe anytime.