02_Compose导航Navigation

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容