一文搞懂Google Navigation Component

2022-12-09,,,

一文搞懂Google Navigation Component

应用中的页面跳转是一个常规任务, Google官方提供的解决方案是Android Jetpack的Navigation component.

本文概括介绍一下基本使用的关键点(详细的how to guide看官方就好了),

结合源码梳理一下基本的navigation component的设计, 帮助大家更好地理解和使用这个库.

Navigation Component基本介绍

首先, 官网的介绍很全面了: https://developer.android.com/guide/navigation

如果想按步骤操作一番请移步官方文档.

这里表扬一下Android Studio, 越来越人性化了.

在添加navigation资源的时候会自动加依赖.

Navigation Editor可以显示destination, 拖拽, 连线加action, 添加编辑参数, 设置动画和返回行为等属性, 提供了一个集中可视化的图.

基本组成部分

Navigation graph: 一般是用xml写(传统的非Compose项目), 放在navigation文件夹下, 其中包含了各个destinations.
NavHost: 一个空的container, 用来展示destinations. Navigation component有一个默认的NavHost实现: NavHostFragment, 用来展示fragment.
NavController: 用来管理navigation. 当告诉NavController想要navigate去哪里, 它就会在NavHost中显示对应的destination.

Navigation Component解决了什么问题呢?

可视化的navigator editor.
导航与实现的解耦. 当A需要跳转到B, A不需要知道B的实现: 到底B节点是一个Activity还是一个Fragment, 它的实现类叫什么.
通过safe args插件提供了类型安全的参数传递.
导航UI(app bar, drawer, bottom navigation): NavigationUI.

不但包含了UI还有与之关联的行为.

Navigation Component对应的Single Activity架构思想

从Navigation Component推出之初的宣传视频, 比如这个, 可以看出它和single activity的思想是紧密结合的.

所以官方推荐的经典做法是这样:

一个activity和多个fragments: activity关联一个navigation graph, 包含一个NavHostFragment, 用来放置不同的fragments.

多个Activity怎么办

当然具体的应用可以选择自己想要的方式, 适合自己的才是最好的.

如果有多个activity, 那么每个activity有自己的navigation graph.

以这个简单的例子举例:

如果Login和Main页面是两个Activity, 它们各自的layout里都有一个NavHostFragment, 这样做的目的有两个:

处理LoginActivity和MainActivity各自内部的页面导航, 比如内部的Fragment切换等.
获取NavController. (具体原因请看获取NavController的几种方式.)

它们又都有各自的navigation graph, 里面列出了可以到达的结点.

因为我们只能到达在同一个graph中列出的节点.

这里LoginActivity需要跳转到MainActivity, 所以在navigation graph中有mainActivity的destination结点.

如果MainActivity也需要跳转到LoginActivity, 就需要在自己的navigation graph中增加一个loginActivity的destination结点.

顶级页面非全屏/子页面全屏的处理

有一个具体的应用case是, 如果app的主要入口是非全屏的(有共享UI部分, 比如bottom bar), 而部分页面需要全屏, 应该如何处理.

比较简单的一种方式就是如上面的例子, 把全屏的页面放在一个单独的Activity. 但这样就会导致很多Activity的出现.

另外一种方式是动态处理nav host和bottom navigation的布局.

比如需要显示一个全屏的Fragment的时候, bottom bar消失, nav host布局充满屏幕.

这就涉及到一些UI的操作和恢复, 可能还需要动画过渡.

多module项目中的导航

当项目慢慢变大之后, 我们会拆分module来组织代码, 除了基础组件的拆分, 各个feature也可能会拆到不同的module中去.

官方建议的方式, 如图所示, app module作为总入口, 依赖feature modules.

navigation graph也放在app module中.

因为navigation graph是支持嵌套和include的, 即navigation里面也可以嵌套navigation, 子的navigation有自己的start destination.

所以navigation graph也可以拆分, 各个module管理自己的navigation graph, 最终include到app module中去.

跨module导航的行为, 是deep link的方式.

具体代码见navigation-multi-module

源码实现是通过字符串匹配找到destination, 然后根据具体的类型找到navigator进行导航.

需要注意, 即便是app module, 它想导航到一个比较深的结点, 推荐的方式也是通过deep link.

当我们嵌套navigation时, 总navigation图的可见结点只到子graph为之, 其内部结点都不可见, 导航会发生destination找不到的错误.

大多数情况, app module也只关心几个入口结点.

跨module导航还有一个缺点是safe args不支持.

Navigation Component源码

NavHost和NavController

NavHost接口的唯一实现类是:NavHostFragment.

NavHostFragment中创建了NavController, 这里也是所有方法最终获得到的NavController的来源.

通过Fragment的生命周期onCreate()触发了graph的创建.

NavController负责了导航行为的控制.

NavController中有很多navigate()方法的重载, 可以根据不同的参数进行导航.

popBackStack()是回退操作.

最终的实现都是从destination中获取到navigator的名字, 然后调用具体的Navigator的navigate()popBackStack()方法.

NavHostControllerNavController的子类, 提供了一些连接外部依赖的设置方法.

App通常不会构造controller, 而是从navigation host获取.

NavController中有字段NavigatorProvider, 而NavigatorProvider中有一个navigators的HashMap.

NavDestination和Navigator

NavDestination

NavDestination是一个描述不同目的地的数据结构基类.

具体实现在不同类型的Navigator中都有对应的类.

NavGraph也是NavDestination的子类. 只不过NavGraph中记录了destination节点信息.

Navigator

Navigator是一个抽象类.

包含的方法中对应导航行为和回退行为的是:

navigate()
popBackStack()

当然还有一个createDestination()的方法负责了destination的创建.

下面几种子类: 对应不同destination的导航.

ActivityNavigator.
FragmentNavigator.
DialogFragmentNavigator.

这个子类:

NavGraphNavigator. 是一个针对NavGraph的元素. 会导航到graph的start destination. 当然具体导航行为会由具体元素类型的provider执行.

可以查看这几个类的导航实现.

比如点进FragmentNavigatornavigate()方法实现, 我们就会发现最终执行的是replace()操作.

Navigation component是支持自定义Navigator的, 我们可以仿照这个类写出自己的版本, 达到定制化的目的.

初始化和导航过程

初始化过程

导航的setup过程大致如下:

这里展示的是xml的navigation graph, 其中解析xml的工作由NavInflator来完成.

解析完成后由navigator进行具体的destination类型创建.

这里graph创建完成之后还会导航到start destination.

导航跳转过程

要跳转到具体某个destination时, 流程如下:

这里解释了为什么只能导航到同一个图下的目的地.

以及最终的导航动作, 是找到对应destination的navigator实现来进行的.

这样对NavController来说就不必关心具体实现.

获取NavController的几种方式

获取NavController的方式有三种(先不说Compose).

第一种: Activity

fun Activity.findNavController(@IdRes viewId: Int): NavController =
Navigation.findNavController(this, viewId)

参数传入view的id. 之后会调用findViewNavController()

第二种: Fragment

fun Fragment.findNavController(): NavController =
NavHostFragment.findNavController(this)

首先向根部遍历, 找到NavHostFragment, 然后getNavController().

找不到还会尝试在view中找, 或者在dialog的view中找.

当然如果拿得到NavHostFragment可以直接get.

第三种: View

fun View.findNavController(): NavController =
Navigation.findNavController(this)

最后的本质依然是调用到了findViewNavController().

不断递归找view的parent, 然后getNavController, 找到为止.

这个地方NavController是写在View的tag里.

查了一下这个方法的调用是NavHostFragmentonViewCreated()里.

findNavController的几种方式总结

所以以上提到的这三种方式, 归根结底是要找到NavHostFragment中的那个NavController.

DSL和Jetpack Compose Navigation

DSL

navigation component还提供了DSL的方式来声明graph, 取代xml的版本.

这种方式可以用于动态构建一个navigation graph.

代码看起来像这样:

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
startDestination = mav_routes.home
) {
fragment<HomeFragment>(nav_routes.home) {
label = resources.getString(R.string.home_title)
} fragment<PlantDetailFragment>(${nav_routes.plant_detail}/${nav_arguments.plant_id}) {
label = resources.getString(R.string.plant_detail_title)
argument(nav_arguments.plant_id) {
type = NavType.StringType
}
}
}

DSL方式的局限性也是不能和safe args结合.

Jetpack Compose Navigation

Compose版本的navigation包是: androidx.navigation:navigation-compose.

有了前面的铺垫, 我们可以发现compose导航库的实现是DSL版本的写法, 结合新的ComposeNavigator.

NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}

所以同样的, NavController要和一个NavHost关联, NavHost其中有一个navigation graph定义了所有的destinations.

每个destination有一个唯一的route字符串来定义自己的路径.

navigation graph同样也可以嵌套.

并且和View的Navigation Component是有Interoperability支持的.

结论

Navigation Component是一个很基础却很有意思的库.

它封装了导航行为, 方便了开发者调用, 也解耦了导航动作和具体结点的实现类.

解决了参数传递的类型安全问题.

提供了可视化的导航图编辑预览工具.

提供了导航UI组件并提供了默认行为, 让开发者直接获得符合设计的默认效果.

它的设计跟单Activity的架构相关, 支持拓展destination类型, 支持dsl写法.

本文结合源码讨论了一下这个库的设计和使用的关键点, 希望对大家有帮助.

References

Navigation Component
Navigation Kotlin DSL
Navigating with Compose

一文搞懂Google Navigation Component的相关教程结束。

《一文搞懂Google Navigation Component.doc》

下载本文的Word格式文档,以方便收藏与打印。