compose-navigation▌
new-silvermoon/awesome-android-agent-skills · updated Apr 8, 2026
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.
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
@Serializableroutes for type safety - Pass only IDs/primitives as arguments
- Use
popUpTowithlaunchSingleTopfor bottom navigation - Extract
NavHostto a separate composable for testability - Use
SavedStateHandle.toRoute<T>()in ViewModels
DON'T
- Pass complex objects as navigation arguments
- Create
NavControllerinsideNavHost - Navigate in
LaunchedEffectwithout proper keys - Forget
FLAG_IMMUTABLEfor PendingIntents (Android 12+) - Use string-based routes (legacy pattern)
References
Discussion
Product Hunt–style comments (not star reviews)- No comments yet — start the thread.
Ratings
4.7★★★★★40 reviews- ★★★★★Henry Johnson· Dec 20, 2024
Keeps context tight: compose-navigation is the kind of skill you can hand to a new teammate without a long onboarding doc.
- ★★★★★Shikha Mishra· Dec 16, 2024
We added compose-navigation from the explainx registry; install was straightforward and the SKILL.md answered most questions upfront.
- ★★★★★Amelia Lopez· Dec 16, 2024
Registry listing for compose-navigation matched our evaluation — installs cleanly and behaves as described in the markdown.
- ★★★★★Carlos Liu· Dec 16, 2024
Useful defaults in compose-navigation — fewer surprises than typical one-off scripts, and it plays nicely with `npx skills` flows.
- ★★★★★Sakshi Patil· Nov 15, 2024
Useful defaults in compose-navigation — fewer surprises than typical one-off scripts, and it plays nicely with `npx skills` flows.
- ★★★★★Li Sanchez· Nov 11, 2024
I recommend compose-navigation for anyone iterating fast on agent tooling; clear intent and a small, reviewable surface area.
- ★★★★★Yash Thakker· Nov 7, 2024
compose-navigation fits our agent workflows well — practical, well scoped, and easy to wire into existing repos.
- ★★★★★Daniel Gill· Nov 7, 2024
compose-navigation reduced setup friction for our internal harness; good balance of opinion and flexibility.
- ★★★★★Dhruvi Jain· Oct 26, 2024
compose-navigation is among the better-maintained entries we tried; worth keeping pinned for repeat workflows.
- ★★★★★Nia Park· Oct 26, 2024
I recommend compose-navigation for anyone iterating fast on agent tooling; clear intent and a small, reviewable surface area.
showing 1-10 of 40