compose-navigation

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "compose-navigation" with this command: npx skills add new-silvermoon/awesome-android-agent-skills/new-silvermoon-awesome-android-agent-skills-compose-navigation

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)

  1. Create NavController

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

AppNavHost(navController = navController)

}

  1. 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&#x3C;Profile> { backStackEntry ->
        val profile: Profile = backStackEntry.toRoute()
        ProfileScreen(userId = profile.userId)
    }
    
    composable&#x3C;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&#x3C;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&#x3C;Profile>()

val user: StateFlow&#x3C;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&#x3C;AuthGraph>(startDestination = Login) {
    composable&#x3C;Login> {
        LoginScreen(
            onLoginSuccess = {
                navController.navigate(Home) {
                    popUpTo&#x3C;AuthGraph> { inclusive = true }
                }
            }
        )
    }
    composable&#x3C;Register> { RegisterScreen() }
    composable&#x3C;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&#x3C;Home>() == true,
            onClick = { navController.navigate(Home) }
        )
        item(
            icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") },
            label = { Text("Settings") },
            selected = currentDestination?.hasRoute&#x3C;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&#x3C;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

  • Navigation with Compose

  • Type-Safe Navigation

  • Pass Data Between Destinations

  • Test Navigation

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

gradle-build-performance

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

compose-ui

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

kotlin-concurrency-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

android-testing

No summary provided by upstream source.

Repository SourceNeeds Review