一文搞懂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. 2021广东工业大学十月月赛 F-hnjhd爱序列

    题目:GDUTOJ | hnjhd爱序列 (gdutcode.cn) 一开始是用双指针从尾至头遍历,但发现会tle!! 后来朋友@77给出了一种用桶的做法,相当于是用空间换时间了. 其中用到的一个原理 ...

  2. spring boot-jpa整合QueryDSL来简化复杂操作

    spring boot-jpa整合QueryDSL来简化复杂操作 SpringDataJPA+QueryDSL玩转态动条件/投影查询  

  3. Linux学习 - 变量测试与内容替换

    变量置换方式 变量y没有设置 变量y为空 变量y有值 x=${y-新值} x=新值 x空 x=$y x=${y:-新值} x=新值 x=新值 x=$y x=${y+新值} x空 x=新值 x=新值 x ...

  4. Linux学习 - 数值运算

    1 declare 声明变量类型 declare [+/-] [选项] 变量名 - 给变量设定类型属性 + 取消变量的类型属性 -i 将变量声明为整数型 -x 将变量声明为环境变量(同export) ...

  5. 基于DataX将数据从Sqlserver同步到Oracle

    DataX是阿里云推出的一款开源的ETL工具,通过配置json文件实现不同数据库之间的数据同步.先有需求是从Sqlserver同步数据到Oracle,网上关于DataX的介绍很多. 框架设计 Data ...

  6. iOS-调用系统的短信和发送邮件功能,实现短信分享和邮件分享

    一.邮件分享 1.iOS系统自带邮件设置邮箱(此处以QQ邮箱为例)(http://jingyan.baidu.com/album/6181c3e084cb7d152ef153b5.html?picin ...

  7. Spring Cloud声明式调用Feign负载均衡FeignClient详解

    为了深入理解Feign,下面将从源码的角度来讲解Feign.首先来看看FeignClient注解@FeignClient的源码,代码如下: FeignClient注解被@Target(ElementT ...

  8. mysql 索引 零记

    索引算法 二分查找法/折半查找法 伪算法 : 1. 前提,数据需要有序 2. 确定数据中间元素 K 3. 比如目标元素 A与K的大小 3.1 相等则找到 3.2  小于时在左区间 3.3  大于时在右 ...

  9. Linux基础命令---htpasswd创建密码文件

    htpasswd htpasswd指令用来创建和更新用于基本认证的用户认证密码文件.htpasswd指令必须对密码文件有读写权限,否则会返回错误码. 此命令的适用范围:RedHat.RHEL.Ubun ...

  10. 【C/C++】习题3-2 分子量/算法竞赛入门经典/字符串

    给出一种物质的分子式,求分子量.只包含4种原子,分别为C,H,O,N. [知识点] 1.ASCII码 [阿拉伯数字]48~57 [大写字母]65~90 [小写字母]97~122 2.输入循环到n-1的 ...