导航Navigation
(1)依赖
在Composable之间进行切换,就需要用到导航Navigation组件。它是一个库,并不是系统Framework里的,所以在使用前,需要添加依赖,如下:
dependencies {
def nav_version = "2.5.3"
implementation("androidx.navigation:navigation-compose:$nav_version")
}
(2)NavController
NavController是导航组件的中心API,它是有状态的。通过Stack保存着各种Composable组件的状态,以方便在不同的Screen之间切换。创建一个NavController的方式如下:
val navController = rememberNavController()
(3)NavHost
每一个NavController都必须关联一个NavHost组件。NavHost像是一个带着导航icon的NavController。每一个icon(姑且这么叫,也可以是name)都对应一个目的页面(Composable组件)。这里引进一个新术语:路由Route,它是指向一个目的页面的路径,可以有很深的层次。使用示例:
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
切换到另外一个Composable组件:
navController.navigate("friendslist")
在导航前,清除某些back stack:
// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home")
}
清除所有back stack,包括"home":
// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home") { inclusive = true }
}
singleTop模式:
// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
launchSingleTop = true
}
这里再引进一个术语:单一可信来源原则,the single source of truth principle。应用在导航这里,即是说导航的切换应该尽可能的放在更高的层级上。例如,一个Button的点击触发了页面的跳转,你可以把跳转代码写在Button的onClick回调里。可如果有多个Button呢?或者有多个触发点呢?每个地方写一次固然可以实现相应的功能,但如果只写一次不是更好吗?通过将跳转代码写在一个较高层级的函数里,传递相应的lambda到各个触发点,就能解决这个问题。示例如下:
@Composable
fun MyAppNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = "profile"
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable("profile") {
ProfileScreen(
onNavigateToFriends = { navController.navigate("friendsList") },
/*...*/
)
}
composable("friendslist") { FriendsListScreen(/*...*/) }
}
}
@Composable
fun ProfileScreen(
onNavigateToFriends: () -> Unit,
/*...*/
) {
/*...*/
Button(onClick = onNavigateToFriends) {
Text(text = "See friends list")
}
}
上面示例的跳转,是在名为"profile"的Composable里。比起Button,这是一个相对较高的层级。
这里再引入一个术语:状态提升hoist state,将可组合函数(Composable Function,后续简称CF)暴露给Caller,该Caller知道如何处理相应的逻辑,是状态提升的一种实践方式。例如上例中,将Button的点击事件,暴露给了NavHost。也即是说,如果需要参数,也是在NavHost中处理,不需要关心具体Button的可能状态。
(4)带参数的导航
导航是可以携带参数的,使用语法如下:
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
使用确切的类型:
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
}
其中navArgument()方法创建的是一个NamedNavArgument对象。
如果想提取参数,那么:
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
其中backStackEntry是NavBackStackEntry类型的。
导航时,传入参数:
navController.navigate("profile/user1234")
注意点:使用导航传递数据时,应该传递一些简单的、必要的数据,如标识ID等。传递复杂的数据是强烈不建议的。如果有这样的需求,可以将这些数据保存在数据层。导航到新页面后,根据ID到数据层获取。
(5)可选参数
添加可选参数,有两点要求。一是必须使用问号语法,二是必须提供默认值。示例如下:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
(6)深层链接Deep Link
Deep Link可以响应其他页面或者外部App的跳转。实现自定义协议是它的使用场景之一。一个示例:
val uri = "https://www.example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
navDeepLink函数会创建一个NavDeepLink对象,它负责管理深层链接。
但是上面这种方式只能响应本App内的跳转,如果想接收外部App的请求,需要在manifest中配置,如下:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
Deep link也适用于PendingIntent,示例:
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
(7)嵌套导航
一些大的模块,可能会包含许多小的模块。那么此时就需要用到嵌套导航了。嵌套导航有助于模块化的细致划分。使用示例:
NavHost(navController, startDestination = "home") {
...
// Navigating to the graph via its route ('login') automatically
// navigates to the graph's start destination - 'username'
// therefore encapsulating the graph's internal routing logic
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
将它作为一个扩展函数实现,以方便使用,如下:
fun NavGraphBuilder.loginGraph(navController: NavController) {
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
}
在NavHost中使用它:
NavHost(navController, startDestination = "home") {
...
loginGraph(navController)
...
}
(8)与底部导航栏的集成
先添加底部导航栏所需的依赖:
dependencies {
implementation("androidx.compose.material:material:1.3.1")
}
android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
kotlinOptions {
jvmTarget = "1.8"
}
}
创建sealed Screen ,如下:
sealed class Screen(val route: String, @StringRes val resourceId: Int) {
object Profile : Screen("profile", R.string.profile)
object FriendsList : Screen("friendslist", R.string.friends_list)
}
BottomNavigationItem需要用到的items:
val items = listOf(
Screen.Profile,
Screen.FriendsList,
)
最终示例:
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
composable(Screen.Profile.route) { Profile(navController) }
composable(Screen.FriendsList.route) { FriendsList(navController) }
}
}
(9)NavHost的使用限制
如果想用NavHost作为导航,那么必须所有的组件都是Composable。如果是View和ComposeView的混合模式,即页面即有原来的View体系,又有ComposeView,是不能使用NavHost的。这种情况下,使用Fragment来实现。
Fragment中虽然不能直接使用NavHost,但也可以使用Compose导航功能。首先创建一个Composable项,如下:
@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
然后,在Fragment中,使用它来实现导航功能,如下:
class MyFragment : Fragment() {
override fun onCreateView(/* ... */): View {
return ComposeView(requireContext()).apply {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
}
}
Over !