导航与路由:Navigation Compose入门
导航,说白了就是页面之间的跳转逻辑。在传统Android开发中,我们用FragmentManager或者Intent来做这件事。但在Jetpack Compose的世界里,Navigation Compose成了官方推荐方案。
我第一次接触Navigation Compose时,心里其实有点抵触——毕竟Fragment那套东西用了好多年。但真正上手后,我发现这套方案简洁得让人舒服。你想想看,不用再写一堆FragmentTransaction的样板代码,多好。
Navigation Compose的核心概念
Navigation Compose的核心就三个东西:NavController、NavHost和路由。
- NavController:导航的总指挥,负责记录页面栈、执行跳转和返回
- NavHost:导航的容器,定义了哪些路由对应哪些Composable页面
- 路由:每个页面的唯一标识,说白了就是一个字符串
我习惯把路由定义成常量,放在一个单独的文件里。这样后期维护时,改一个地方就行,不用满项目搜索字符串。
object Routes {
const val HOME = "home"
const val PROFILE = "profile/{userId}"
const val SETTINGS = "settings"
}
NavHost与NavController的用法
先看一个最简单的例子。我们在Activity里创建一个NavController,然后把它传给NavHost。
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Routes.HOME
) {
composable(Routes.HOME) {
HomeScreen(
onNavigateToProfile = { userId ->
navController.navigate("profile/$userId")
}
)
}
composable(
route = Routes.PROFILE,
arguments = listOf(
navArgument("userId") { type = NavType.StringType }
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId") ?: ""
ProfileScreen(userId = userId)
}
}
}
这里有个细节要注意:rememberNavController()必须在Composable作用域内调用。我曾经在ViewModel里尝试创建NavController,结果编译直接报错——它依赖CompositionLocal,只能在Composable环境里用。
参数传递
参数传递分两种:路径参数和查询参数。
| 参数类型 | 写法 | 适用场景 |
|---|---|---|
| 路径参数 | profile/{userId} |
必须传递的参数,如用户ID |
| 查询参数 | search?query={keyword} |
可选参数,如搜索关键词 |
路径参数在路由定义时用花括号占位,然后在navArgument里声明类型。查询参数则用?拼接,类似URL的query string。
// 路径参数示例
composable(
route = "profile/{userId}",
arguments = listOf(
navArgument("userId") {
type = NavType.StringType
defaultValue = "unknown"
}
)
)
// 查询参数示例
composable(
route = "search?query={query}",
arguments = listOf(
navArgument("query") {
type = NavType.StringType
nullable = true
}
)
)
嗯,这里要注意:路径参数不能为null,但查询参数可以。如果你需要传递可选值,用查询参数更合适。
深层链接
深层链接(Deep Link)允许外部应用直接跳转到你App的某个页面。比如用户点击一个链接,直接打开商品详情页。
配置深层链接分两步:
- 在AndroidManifest.xml里声明intent-filter
- 在NavHost的composable里添加deepLinks参数
<!-- AndroidManifest.xml -->
<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="myapp"
android:host="profile"
android:pathPrefix="/{userId}" />
</intent-filter>
</activity>
// NavHost中的深层链接配置
composable(
route = "profile/{userId}",
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://profile/{userId}" }
)
) { backStackEntry ->
// 处理页面逻辑
}
我曾经踩过一个坑:深层链接的uriPattern必须和AndroidManifest里的scheme、host、path完全匹配,否则死活跳不过去。调试时记得用adb命令测试:adb shell am start -d "myapp://profile/123"。
底部导航栏实现
底部导航栏是大多数App的标配。在Navigation Compose里实现它,核心思路是:用Scaffold包裹BottomNavigation,然后和NavHost联动。
data class BottomNavItem(
val route: String,
val label: String,
val icon: ImageVector
)
val bottomNavItems = listOf(
BottomNavItem(Routes.HOME, "首页", Icons.Default.Home),
BottomNavItem(Routes.SEARCH, "搜索", Icons.Default.Search),
BottomNavItem(Routes.PROFILE, "我的", Icons.Default.Person)
)
@Composable
fun MainScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
bottomNavItems.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = Routes.HOME,
modifier = Modifier.padding(innerPadding)
) {
composable(Routes.HOME) { HomeScreen() }
composable(Routes.SEARCH) { SearchScreen() }
composable(Routes.PROFILE) { ProfileScreen() }
}
}
}
这里有个关键点:popUpTo和launchSingleTop的组合。如果不加这两个参数,每次点击底部导航都会创建一个新的页面实例,返回时就会出现奇怪的行为。我刚开始做的时候没注意,结果用户反馈说「点返回键直接退出App了」——其实就是页面栈乱了。
核心要点:底部导航的跳转一定要用popUpTo(startDestinationId) { saveState = true } + launchSingleTop = true + restoreState = true。这能保证页面栈干净,状态也能正确恢复。
知识体系总览
下面这张图概括了本章的核心内容。我画图时习惯把关键流程和注意事项都标出来,方便复习时一眼看到重点。
个人建议:刚开始学Navigation Compose时,不要急着把所有功能都塞进去。先跑通一个最简单的两页面跳转,再逐步添加参数传递、深层链接、底部导航。我每次带新人都是这个思路——先跑通,再优化。
注意:Navigation Compose的版本和Compose版本需要匹配。我用的是androidx.navigation:navigation-compose:2.7.7,对应Compose BOM 2024.02.00。版本不匹配会导致编译错误,或者运行时出现奇怪的行为。
好了,这一章的内容就到这里。导航是App的骨架,把骨架搭好了,后面的页面填充就顺理成章。下一章我们会深入Compose的布局系统,看看那些好用的布局组件到底怎么用。
公众号:蓝海资料掘金营,微信deep3321