02_Compose导航Navigation

2022-12-31  本文已影响0人  刘加城

导航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 !

上一篇 下一篇

猜你喜欢

热点阅读