compose-navigation by new-silvermoon/awesome-android-agent-skills
npx skills add https://github.com/new-silvermoon/awesome-android-agent-skills --skill compose-navigation使用 Navigation Compose 库在 Jetpack Compose 应用程序中实现类型安全的导航。此技能涵盖 NavHost 设置、参数传递、深层链接、嵌套图、自适应导航和测试。
添加 Navigation Compose 依赖项:
// build.gradle.kts
dependencies {
implementation("androidx.navigation:navigation-compose:2.8.5")
// 用于类型安全导航(推荐)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
// 启用序列化插件
plugins {
kotlin("plugin.serialization") version "2.0.21"
}
使用 @Serializable 数据类/对象实现类型安全路由:
import kotlinx.serialization.Serializable
// 简单屏幕(无参数)
@Serializable
object Home
// 带必需参数的屏幕
@Serializable
data class Profile(val userId: String)
// 带可选参数的屏幕
@Serializable
data class Settings(val section: String? = null)
// 带多个参数的屏幕
@Serializable
data class ProductDetail(val productId: String, val showReviews: Boolean = false)
@Composable
fun MyApp() {
val navController = rememberNavController()
AppNavHost(navController = navController)
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
@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)
}
}
}
// 向前导航
navController.navigate(Profile(userId = "user123"))
// 导航并弹出当前屏幕
navController.navigate(Home) {
popUpTo<Home> { inclusive = true }
}
// 返回导航
navController.popBackStack()
// 导航回特定目标
navController.popBackStack<Home>(inclusive = false)
navController.navigate(Profile(userId = "user123")) {
// 弹出到目标(清除返回堆栈)
popUpTo<Home> {
inclusive = false // 将 Home 保留在堆栈中
saveState = true // 保存弹出屏幕的状态
}
// 避免同一目标的多个副本
launchSingleTop = true
// 导航到此目标时恢复状态
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
}
}
)
// 添加更多项...
}
}
) { innerPadding ->
AppNavHost(
navController = navController,
modifier = Modifier.padding(innerPadding)
)
}
}
composable<Profile> { backStackEntry ->
val profile: Profile = backStackEntry.toRoute()
ProfileScreen(userId = profile.userId)
}
@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)
}
// 正确:仅传递 ID
navController.navigate(Profile(userId = "user123"))
// 在 ViewModel 中,从仓库获取
class ProfileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
val user = userRepository.getUser(profile.userId)
}
// 错误:不要传递复杂对象
// navController.navigate(Profile(user = complexUserObject)) // 错误!
@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() }
// 用于身份验证流程的嵌套图
navigation<AuthGraph>(startDestination = Login) {
composable<Login> {
LoginScreen(
onLoginSuccess = {
navController.navigate(Home) {
popUpTo<AuthGraph> { inclusive = true }
}
}
)
}
composable<Register> { RegisterScreen() }
composable<ForgotPassword> { ForgotPasswordScreen() }
}
}
// 路由定义
@Serializable object AuthGraph
@Serializable object Login
@Serializable object Register
@Serializable object ForgotPassword
使用 NavigationSuiteScaffold 实现响应式导航(手机底部栏,平板侧边栏):
@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 路由实现类型安全launchSingleTop 的 popUpToNavHost 提取到单独的可组合函数以提高可测试性SavedStateHandle.toRoute<T>()NavHost 内部创建 NavControllerLaunchedEffect 中导航FLAG_IMMUTABLE(Android 12+)每周安装量
163
仓库
GitHub 星标数
552
首次出现
2026年1月27日
安全审计
安装于
codex144
opencode143
gemini-cli125
github-copilot121
kimi-cli114
amp113
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)
}
@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)
}
// 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+)Weekly Installs
163
Repository
GitHub Stars
552
First Seen
Jan 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex144
opencode143
gemini-cli125
github-copilot121
kimi-cli114
amp113
Kotlin Exposed ORM 模式指南:DSL查询、DAO、事务管理与生产配置
913 周安装
Java Spring开发最佳实践指南:Spring Boot 3.x微服务架构与REST API设计
161 周安装
Sequelize ORM 开发指南:Node.js 数据库模型关联与迁移实战
161 周安装
OilOil UI/UX 设计指南与审查工具:现代极简风格设计原则与代码审查
161 周安装
Symfony端口与适配器模式:优化架构、安全重构与工作流管理指南
161 周安装
靶标研究工具:9条路径全面收集药物靶点情报,支持基因符号、UniProt ID识别
161 周安装
Claude AI 檔案規劃系統:使用 Markdown 檔案作為持久化工作記憶的開發者技能
161 周安装