client/.github/skills/compose-navigation/SKILL.md
Implement navigation in Jetpack Compose using Navigation Compose. Use when asked to set up navigation, pass arguments between screens, handle deep links, or structure multi-screen apps.
npx skillsauth add ahaodev/heji compose-navigationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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"
}
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)
@Composable
fun MyApp() {
val navController = rememberNavController()
AppNavHost(navController = navController)
}
@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)
}
}
}
// 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)
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
}
@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)
)
}
}
composable<Profile> { backStackEntry ->
val profile: Profile = backStackEntry.toRoute()
ProfileScreen(userId = profile.userId)
}
class ProfileViewModel(
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)
}
// 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!
@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)
}
<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>
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)
}
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
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)
}
}
// build.gradle.kts
androidTestImplementation("androidx.navigation:navigation-testing:2.8.5")
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
)
}
}
@Serializable routes for type safetypopUpTo with launchSingleTop for bottom navigationNavHost to a separate composable for testabilitySavedStateHandle.toRoute<T>() in ViewModelsNavController inside NavHostLaunchedEffect without proper keysFLAG_IMMUTABLE for PendingIntents (Android 12+)development
Apply Shadmin feature-development standards (backend Go/Gin/Ent + frontend React/TS). Use when adding/modifying features, CRUD modules, API routes/controllers/usecases/repositories, Ent schemas, or web pages/routes.
data-ai
Convert Android XML layouts to Jetpack Compose. Use when asked to migrate Views to Compose, convert XML to Composables, or modernize UI from View system to Compose.
development
Kotlin Coroutines review and remediation for Android. Use when asked to review concurrency usage, fix coroutine-related bugs, improve thread safety, or resolve lifecycle issues in Kotlin/Android code.
development
Debug and optimize Android/Gradle build performance. Use when builds are slow, investigating CI/CD performance, analyzing build scans, or identifying compilation bottlenecks.