Google在2018年就推出了Jetpack组件库,但是直到今天我才给重视起来,这真的不得不说是一件让人遗憾的事。过去几年的空闲时间里,我一直在尝试做一套自己的组件库,帮助自己快速开发,虽然也听说过Jetpack,但是压根儿也没去了解,但是其实自己已经无形之中用到过很多Jetpack中的库了,只是自己不知道,比如说databinding、viewmodel、camerax等等
所以我打算推出一个Jetpack的学习记录,今天是第一个组件:Navigation
据通义千问的说法:
Android Jetpack Navigation组件是Google推出的一个用于简化Android应用导航的库。旨在提供一种结构化和统一的方式来管理应用程序中的屏幕切换和导航流程,特别是对于基于Fragment的应用。
关于Navigation,我觉得可能大家在生活里对于它的功能并不会陌生,拿微信来说,底部有四个按钮,分别是“微信”、“通讯录”、“发现”、“我”,如下图
当我们分别点击四个按钮的时候,界面区域也会随之切换到对应的页面。这就是一种比较常见的底部导航功能。当然这种结合底部导航栏的fragment切换只是Navigation能够实现的其中一种,还有其他的并不需要底部导航栏的,比如说登录模块,登录模块可能包含着登录、注册和重置密码这三个子模块,那么按照UI的设计,就需要三张页面去实现,我们知道可以使用一个Activity去嵌套三个Fragment去实现,那么显然这种纯fragment切换,这也是Navigation可以实现的范畴
今天展示Navigation的使用的demo的整体设计思路为:LoginActivity+MainActivity
其中LoginActivity包含着LoginFragment、RegisterFragment、ResetPasswordFragment
其中MainActivity包含HomeFragment、ContactFragment、FindFragment、MeFragment
我的环境:AndroidStudio 4.2.2
Project级build.gradle
dependencies {
...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.0"
...
}
App级build.gradle
plugins {
....
id 'androidx.navigation.safeargs.kotlin'
}
dependencies {
...
//Navigation
implementation "androidx.navigation:navigation-fragment-ktx:2.4.2"
implementation "androidx.navigation:navigation-ui-ktx:2.4.2"
}
步骤如下:
按照这样的步骤,即可创建login_nav_graph.xml和main_nav_graph.xml两个导航文件,如图:
此时,两份文件里内容还是空的,具体内容还需要自行添加
AndroidStudio支持可视化添加fragment以及相互之间的导航关系,这点真的是非常方便
我们首先把使用到的fragment全部添加到导航文件中,如下图:
这里先看一下AndroidStudio自动为我们生成的代码
这个fragment标签,与我们拖进来的三个fragment是一一对应的,它包含了四个参数
- id:比唯一标识符,可供本文件其他地方调用
- name:是对应Fragment的路径
- label:是对应Fragment的一个标签
- tools:layout:对应Fragment的布局文件
还有另一个属性也是值得关注的,就是最外层navigation标签的
app:startDestination="@id/loginFragment"
这表明了,在LoginActivity中,默认优先显示的是LoginFragment,
说明:我们首次添加的Fragment会被默认为优先显示的Fragment,即如果我这里优先添加LoginFragment到导航图,navigation自动生成app:startDestination="@id/loginFragment",那么如果我首先把RegisterFragment添加进导航图,那么这个属性就会是app:startDestination="@id/registerFragment"
点LoginFragment,框体右边中部会出现一个圆环
点击该圆环,并拖动,就会出现一条蓝线,将该蓝线指向RegisterFragment后松手
此时就会看到,LoginFragment到RegisterFragment的导航关系被建立了,观察新增的action代码
- id:唯一标识符,可供Activity调用
- destination:用来表示跳转的目的地,可见此时这个action表示的是跳转到registerFragment。需要说明的是,这个跳转每次都会新建实例,也就是我可以从LoginFragment跳转到LoginFragment,但是这两个LoginFragment是不同的实例,也就是栈内会同时存在两个不同的LoginFragment。
其他的属性:
- app:launchSingleTop:类似于Android活动中
singleTop
启动模式,当该属性为true时,如果目标Fragment已经在回退栈的顶部(即用户最近访问的Fragment),那么Navigation组件将不会创建新的Fragment实例,而是重用已经存在的那个Fragment实例。如果目标Fragment已经在回退栈中,但不在栈顶,那么app:launchSingleTop
属性将不会起作用。在这种情况下,即使app:launchSingleTop
设置为true
,Navigation组件也会创建一个新的目标Fragment实例并将其推送到回退栈的顶部。app:popUpTo:
出栈直到某个元素。比如目前栈内有fragment1 - fragment2 - fragment3,当我在fragment4中定义了app:popUpTo:"@id/fragment1"时,那么fragment2和fragment3会被出栈,最终栈内情况为fragment1 - fragment4。- app:popUpToInclusive:这个属性是配合上面的
app:popUpTo使用的,
用来判断到达指定元素时是否把指定元素也出栈。还是以上面的例子来说明,如果该值为true,那么作为指定元素,fragment1也会被出栈,最终栈内只剩下fragment4.
- app:enterAnim、app:exitAnim、app:popEnterAnim、app:popExitAnim:这四个属性都是跳转动画相关的,前两个用来配置移动到目的地的动画,后两个配置离开目的地的动画。
举例说明fragment1跳转到fragment2:
(1)enterAnim和exitAnim发生于fragment1跳转到fragment2的过程中:
enterAnim是fragment2的入场动画、exitAnim是fragment1的离场动画
(2)popEnterAnim和popExitAnim发生于fragment2返回到fragment1的过程中:
popEnterAnim为返回发生后,fragment1的入场动画
popExitAnim为返回时,fragment2的离场动画
按照我们最开始设计的跳转关系去完成导航文件,最终是这样的:
即:默认展示登录页面,登录页面可以跳转到注册页面或者是重置密码页面,同时在注册页面和重置密码页面也可以返回到登陆页面
同理,我们完成main_nav_graph的导航关系,如下
因为我们仿照的是微信的底部导航栏,home、contact、find、me四个fragment其实是平级的,它们之间并不存在导航关系。当然我们也可以根据自己的实际业务使用不同的action来设计不同维度和复杂度的导航关系。
至此,导航文件也有了,剩下的就是创建一个支持导航关系的容器了
以LoginActivity为例
布局文件:
创建FragmentContainerView作为导航主机(Navigation Host),这里有几个地方需要说明:
android:name="androidx.navigation.fragment.NavHostFragment"是固定的写法
FragmentContainerView
标记为默认的导航主机,这意味着这个FragmentContainerView
会拦截系统的后退按钮事件。当用户点击后退按钮时,Navigation组件会按照导航图中的历史记录进行后退操作,而不是直接关闭Activity。并且在一个Activity中,通常只需要一个NavHostFragment
作为导航主机。设置app:defaultNavHost="true"
可以确保只有这一个NavHostFragment
响应导航操作和后退按钮事件,避免多个NavHostFragment
之间的冲突。app:navGraph是将导航文件与导航主机相关联
LoginActivity:
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private lateinit var navController:NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.login_nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
}
public fun getNavController(): NavController {
return navController
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.login_nav_host_fragment).navigateUp()
}
}
核心代码:获取NavController
说明,这里声明了一个方法,将获取到的navController返回了出去,主要是供Fragment中进行调用,因为这里Activity只是容器,具体的UI交互,是在对应的Fragment上面的,以LoginFragment为例:
class LoginFragment : Fragment() {
private lateinit var binding: FragmentLoginBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLoginBinding.inflate(layoutInflater)
val activity = requireActivity() as LoginActivity
navController = activity.getNavController()
initListener()
return binding.root
}
private fun initListener() {
binding.login.setOnClickListener {
val intent = Intent(activity, MainActivity::class.java)
startActivity(intent)
}
binding.register.setOnClickListener {
navController.navigate(R.id.action_loginFragment_to_resetPasswordFragment)
}
binding.reset.setOnClickListener {
navController.navigate(R.id.action_loginFragment_to_resetPasswordFragment)
}
}
}
通过这两行代码,fragment获取到了activity的navcontroller
val activity = requireActivity() as LoginActivity
navController = activity.getNavController()
之后就可以操作导航事件了,如下
binding.register.setOnClickListener {
navController.navigate(R.id.action_loginFragment_to_resetPasswordFragment)
}
binding.reset.setOnClickListener {
navController.navigate(R.id.action_loginFragment_to_resetPasswordFragment)
}
说明,这里面的
R.id.action_loginFragment_to_resetPasswordFragment
和
R.id.action_loginFragment_to_resetPasswordFragment
就是login_nav_graph.xml文件中定义的两个action的id
我相信写到这里,你基本上就能够看出来整个的调用机制了
(4)Navigation+BottomNavigationView实现底部导航
上面讲了普通的fragment切换,那么关于带底部导航栏的切换,也还是很有必要说明以下的
主界面布局
显然,页面中只是增加了BottomNavigationView,对应的UI结构如下
说明:
我这里使用了menu来实现了底部导航栏的几个item内容的导入,代码如下
重点说明:这里面四个item的id并不是随意定义的,一定要与main_nav_graph.xml文件中对应的几个fragment的id保持一致,否则,点击底部导航栏的按钮,是无法触发对应的fragment切换的!!!如下
这里面只是UI上对应了,如何让bottomnavigationview与navcontroller也关联到一起呢
MainActivity.class
class MainActivity : AppCompatActivity() {
private lateinit var binding:ActivityMainBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.main_nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
binding.bottomNavigationView.setupWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.main_nav_host_fragment).navigateUp()
}
}
核心代码就是这一句了:
binding.bottomNavigationView.setupWithNavController(navController)
至此,关于Jetpack组件库中的Navigation导航库的基本使用就是这样了,但是还并不完善,后续我会继续完善本文,把关于fragment间数据传递以及其他的比较高阶的功能增加上来,因为demo还在继续迭代,本文也只是出版,所以源代码暂时先不上传了,但是如果有需要的可以评论区留方式,我看到之后会分享最新的demo源码