一文搞懂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

一文搞懂Google Navigation Component的更多相关文章

  1. 一文搞懂RAM、ROM、SDRAM、DRAM、DDR、flash等存储介质

    一文搞懂RAM.ROM.SDRAM.DRAM.DDR.flash等存储介质 存储介质基本分类:ROM和RAM RAM:随机访问存储器(Random Access Memory),易失性.是与CPU直接 ...

  2. 基础篇|一文搞懂RNN(循环神经网络)

    基础篇|一文搞懂RNN(循环神经网络) https://mp.weixin.qq.com/s/va1gmavl2ZESgnM7biORQg 神经网络基础 神经网络可以当做是能够拟合任意函数的黑盒子,只 ...

  3. 一文搞懂 Prometheus 的直方图

    原文链接:一文搞懂 Prometheus 的直方图 Prometheus 中提供了四种指标类型(参考:Prometheus 的指标类型),其中直方图(Histogram)和摘要(Summary)是最复 ...

  4. Web端即时通讯基础知识补课:一文搞懂跨域的所有问题!

    本文原作者: Wizey,作者博客:http://wenshixin.gitee.io,即时通讯网收录时有改动,感谢原作者的无私分享. 1.引言 典型的Web端即时通讯技术应用场景,主要有以下两种形式 ...

  5. 一文搞懂vim复制粘贴

    转载自本人独立博客https://liushiming.cn/2020/01/18/copy-and-paste-in-vim/ 概述 复制粘贴是文本编辑最常用的功能,但是在vim中复制粘贴还是有点麻 ...

  6. 三文搞懂学会Docker容器技术(中)

    接着上面一篇:三文搞懂学会Docker容器技术(上) 6,Docker容器 6.1 创建并启动容器 docker run [OPTIONS] IMAGE [COMMAND] [ARG...] --na ...

  7. 三文搞懂学会Docker容器技术(下)

    接着上面一篇:三文搞懂学会Docker容器技术(上) 三文搞懂学会Docker容器技术(中) 7,Docker容器目录挂载 7.1 简介 容器目录挂载: 我们可以在创建容器的时候,将宿主机的目录与容器 ...

  8. 一文搞懂所有Java集合面试题

    Java集合 刚刚经历过秋招,看了大量的面经,顺便将常见的Java集合常考知识点总结了一下,并根据被问到的频率大致做了一个标注.一颗星表示知识点需要了解,被问到的频率不高,面试时起码能说个差不多.两颗 ...

  9. 一文搞懂 js 中的各种 for 循环的不同之处

    一文搞懂 js 中的各种 for 循环的不同之处 See the Pen for...in vs for...of by xgqfrms (@xgqfrms) on CodePen. for &quo ...

随机推荐

  1. ython学习笔记(接口自动化框架 V2.0)

    这个是根据上次框架版本进行的优化 用python获取excel文件中测试用例数据 通过requets测试接口.并使用正则表达式验证响应信息内容 生成xml文件测试报告 版本更新内容: 1. 整理了Cr ...

  2. Give You My Best Wishes

    亲耐滴IT童鞋们: 感谢大家一直以来的支持,因为有你们的支持,才有我这么"拼"的动力!!爱你们哟 OC的学习已经告一段落,希望大家通过阅读这几篇浅薄的随笔,能够寻找到解决问题的方法 ...

  3. shell神器curl命令的用法 curl用法实例笔记

    shell神器curl命令的用法举例,如下: ##基本用法(配合sed/awk/grep) $curl http://www.jquerycn.cn ##下载保存 $curl http://www.j ...

  4. SpringBoot服务间使用自签名证书实现https双向认证

    SpringBoot服务间使用自签名证书实现https双向认证 以服务server-one和server-two之间使用RestTemplate以https调用为例 一.生成密钥 需要生成server ...

  5. 『学了就忘』Linux服务管理 — 79、源码包安装的服务管理

    目录 1.源码包服务的启动管理 2.源码包服务的自启动管理 3.让源码包服务被服务管理命令识别 1.源码包服务的启动管理 # 通过源码包的安装路径,找到该服务的启动脚本, # 也就是获得该服务的启动脚 ...

  6. python基础 (三)

    成员运算 判断某个个体在不在某个群体里,关键词:in(在),not in(不在)例如: 特殊的,如果是字典中,因为字典的V值是隐藏的,能查看的只有V,所以无法判断V值,只能判断K值. 身份运算 用于判 ...

  7. Go语言核心36讲(Go语言实战与应用二十六)--学习笔记

    48 | 程序性能分析基础(上) 作为拾遗的部分,今天我们来讲讲与 Go 程序性能分析有关的基础知识. Go 语言为程序开发者们提供了丰富的性能分析 API,和非常好用的标准工具.这些 API 主要存 ...

  8. CF1076B Divisor Subtraction 题解

    Content 给定一个数 \(n\),执行如下操作: 如果 \(n=0\) 结束操作. 找到 \(n\) 的最小质因子 \(d\). \(n\leftarrow n-d\) 并跳到操作 \(1\). ...

  9. LuoguP4419 [COCI2017-2018#1] Cezar 题解

    Content 有一个牌库,有一些点数为 \(1\sim 11\) 的牌,其中除了点数为 \(10\) 的牌有 \(16\) 张之外,其余点数的牌各有四张.现在玩一个游戏,已经拿出了 \(n\) 张牌 ...

  10. CF1501A Alexey and Train 题解

    Content 一列火车从 \(0\) 时刻开始从 \(1\) 号站出发,要经过 \(n\) 个站,第 \(i\) 个站的期望到达时间和离开时间分别为 \(a_i\) 和 \(b_i\),并且还有一个 ...