一、简介
1.定义
什么是Navigation呢?
Navigation是一个可简化Android导航的库和插件。
更确切的来说,Navigation是用来管理Fragment的切换,并且可以通过可视化的方式,看见App的交互流程。
2.优点
- 处理fragment的切换。
- 默认情况下正确处理fragment的前进与后退。
- 为过渡和动画提供标准化的资源。
- 可以绑定
Toolbar、BottomNavigationView和ActionBar等。 SafeArgs(Gradle插件)数据传递时提供类型安全性。ViewModel支持。- 实现和处理深层链接。
3.名词介绍
Navigation中最关键的三要素:
Navigation Graph:只是一个新的资源文件(res下),用户在可视化界面可以看出他能够到达的Destination(屏幕界面),以及流程关系。NavHostFragment:当前fragment的容器。NavControlle:导航的控制者。
二、实战
第一步 添加依赖
ext.navigationVersion = "2.5.3"
dependencies {
//...
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
}
如果你要使用SafeArgs插件,还要在项目目录下的build.gradle文件添加:
buildscript {
ext.navigationVersion = "2.0.0"
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
}
}
以及模块下面的build.gradle文件添加:
apply plugin: 'kotlin-android-extensions'
apply plugin: 'androidx.navigation.safeargs'
第二步 创建navigation导航
1.创建基础目录:资源文件res目录下创建navigation目录 -> 右击navigation目录New一个login_navigation.xml
2.创建一个Destination,如果说navigation是我们的导航工具,Destination就是我们的目的地,在此之前,我已经写好了一个LoginFragment、OneFragment、TwoFragment和ThreeFragment,添加Destination的操作完成后如下所示:
login_navigation.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/navigation"
app:startDestination="@id/loginFragment2">
<fragment
android:id="@+id/loginFragment2"
android:name="com.damaris.mykotlin.LoginFragment"
android:label="fragment_login"
tools:layout="@layout/fragment_login" >
</fragment>
<fragment
android:id="@+id/oneFragment2"
android:name="com.damaris.mykotlin.OneFragment"
android:label="fragment_one"
tools:layout="@layout/fragment_one" >
</fragment>
<fragment
android:id="@+id/twoFragment2"
android:name="com.damaris.mykotlin.TwoFragment"
android:label="fragment_two"
tools:layout="@layout/fragment_two" >
</fragment>
<fragment
android:id="@+id/threeFragment2"
android:name="com.damaris.mykotlin.ThreeFragment"
android:label="fragment_three"
tools:layout="@layout/fragment_three" >
</fragment>
</navigation>
navigation标签的属性:
app:startDestination:默认的起始位置,代表了路由的起点。多个destination连接起来就组成了一个栈导航图,destination之间连接就是action。
每个fragment标签,代表了一层路由,当然,这里不仅仅可以是fragment,也可以是Activity、Dialog。
第三步 建立NavHostFragment
我们需要创建一个MainActivity,activity_main.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
app:navGraph="@navigation/login_navigation"
app:defaultNavHost="true"
android:layout_height="match_parent"
android:layout_marginBottom="10dp" />
</LinearLayout>
FragmentContainerView属性解释:
android:name:值必须是androidx.navigation.fragment.NavHostFragment,代表这个容器就是用来管理Fragment的容器app:navGraph:存放的是第二步建好导航的资源文件,也就是确定了Navigation Graphapp:defaultNavHost="true":代表可以拦截系统的返回键,用来托管路由
FragmentContainerView是一个特殊的Fragment,只能添加Fragment,FragmentContainerView内部会通过反射的方式,初始化名为name所指定的class——NavHostFragment,它就是所有需要管理的Fragment的Container。
第四步 界面跳转、参数传递和动画
方式一:利用ID导航
目标:LoginFragment携带key为name的数据跳转到OneFragment,OneFragment接收后显示。
登录按钮的点击事件如下:
btLogin.setOnClickListener {
val navOption = navOptions {
anim {
enter = R.anim.nav_default_enter_anim
exit = R.anim.nav_default_exit_anim
popEnter = R.anim.nav_default_pop_enter_anim
popExit = R.anim.nav_default_pop_exit_anim
}
}
val bundle = Bundle()
bundle.putString("name", "damaris")
findNavController().navigate(R.id.oneFragment2, bundle, navOption)
}
后续OneFragment的接收代码比较简单,直接获取Fragment中的Bundle即可,这里不再出示代码。
方式二 利用Safe Args
目标:LoginFragment通过Safe Args将数据传到TwoFragment,TwoFragment接收后显示。
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/navigation"
app:startDestination="@id/loginFragment2">
<fragment
android:id="@+id/loginFragment2"
android:name="com.damaris.mykotlin.LoginFragment"
android:label="fragment_login"
tools:layout="@layout/fragment_login" >
<action
android:id="@+id/action_loginFragment2_to_twoFragment2"
app:destination="@id/twoFragment2"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
</fragment>
<fragment
android:id="@+id/oneFragment2"
android:name="com.damaris.mykotlin.OneFragment"
android:label="fragment_one"
tools:layout="@layout/fragment_one" >
</fragment>
<fragment
android:id="@+id/twoFragment2"
android:name="com.damaris.mykotlin.TwoFragment"
android:label="fragment_two"
tools:layout="@layout/fragment_two" >
<argument
android:name="EMAIL"
android:defaultValue="111@qq.com"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/threeFragment2"
android:name="com.damaris.mykotlin.ThreeFragment"
android:label="fragment_three"
tools:layout="@layout/fragment_three" >
</fragment>
</navigation>
enterAnim: 目标Page进入动画exitAnim: 目标Page进入时,原Page退出动画popEnterAnim:目标Page退出动画popExitAnim: 目标Page退出时,原Page退出动画
可以通过在Design界面中,直接选中action来设置,也可以直接在代码中指定。
action标签
app:destination:跳转完成到达的fragment的Idapp:popUpTo:将fragment从栈中弹出,直到某个Id的fragment(之后会有讲解)
argument标签
android:name:标签名字app:argType:标签类型android:defaultValue:默认值
到这步可以发现系统为我们生成了两个类
LoginFragment中的点击事件:
btRegister.setOnClickListener {
val action = LoginFragmentDirections
.actionLoginFragment2ToTwoFragment2()
.setEMAIL("222@qq.com")
findNavController().navigate(action)
}
TwoFragment中的接收:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ...
val safeArgs:TwoFragmentArgs by navArgs()
val email = safeArgs.email
}
需要提及的是,如果不用Safe Args,action可以由Navigation.createNavigateOnClickListener(R.id.next_action, null)方式生成,第二个参数为bundle,这里就不过多阐述了。
三、进阶详解
1.返回控制
navigateUp
navigateUp与物理返回键的功能类似,即返回当前页面堆栈的栈顶页面
findNavController().navigateUp()
当我们从A路由到B,B路由到C后,通过上面的代码,使用navigateUp返回,则路由返回路径为C到B,B到A,如果在A继续调用navigateUp,则不会响应,因为当前栈中只有唯一一个页面,而且是startDestination,所以不会再响应返回操作。
popBackStack
navigateUp只能响应向上一级的路由控制,而不能跨级进行路由返回,popBackStack则是对其的补充,可以指定路由返回的action
findNavController().popBackStack(R.id.loginFragment2,false)
当我们从A路由到B,B路由到C后,通过popBackStack返回,指定要返回到的Fragment的id,即可直接返回到指定位置,第二个参数inclusive,代表返回操作是否包含指定的Fragment id。
这里要注意的是,当你指定返回到A,同时inclusive为true的时候,A也是不会被移除的,因为A是栈顶。
借助popBackStack的返回值,可以在跳转失败时,创建新的Fragment。
val flag = Navigation.findNavController(getView()).popBackStack(R.id.someFragment, false)
if (!flag){
Navigation.findNavController(getView()).navigate(R.id.someFragment)
}
popupTo
当我们通过navigation去进行路由的时候,每次都会创建一个新的实例,所以,当navigation出现下面的循环图时,例如A—B—C—A—B—C,这就导致页面栈中存在了大量重复的页面。
所以在这种场景下,就需要在A—B—C之后,在C—A的路由中,配置popUpTo="@id/A",同时设置popUpToInclusive=true,将旧的A界面也移除,这样,C—A路由之后,页面栈中就只剩下A了(如果是false,则会存在两个A的实例)
<fragment
android:id="@+id/threeFragment2"
android:name="com.damaris.mykotlin.ThreeFragment"
android:label="fragment_three"
tools:layout="@layout/fragment_three" >
<action
android:id="@+id/action_threeFragment2_to_loginFragment2"
app:destination="@id/loginFragment2"
app:popUpTo="@id/loginFragment2"
app:popUpToInclusive="true" />
</fragment>
再考虑下面这样一个场景,A—B,B路由到C的时候,设置popUpTo="@id/A",如果popUpToInclusive=false,则跳转到C之后的路由栈为A—C,如果设置为true,则只剩下C在路由栈中
<fragment
android:id="@+id/oneFragment2"
android:name="com.damaris.mykotlin.OneFragment"
android:label="fragment_one"
tools:layout="@layout/fragment_one" >
<action
android:id="@+id/action_oneFragment2_to_threeFragment2"
app:destination="@id/threeFragment2"
app:popUpToInclusive="true"
app:popUpTo="@id/loginFragment2"/>
</fragment>
这个场景可以使用于登录注册之后跳转主页的场景,当跳转主页后,就应该把登录和注册的界面pop出栈。
所以,从上面的实例就可以分析出,在action中配置popUpTo属性,指的是在当前路由中,一直将页面出栈,直到指定的页面为止,而popUpToInclusive,则是代表包含关系,是否包含指定的页面。
在代码中也有类似的调用方法:
NavOptions.Builder()
.setPopUpTo(R.id.loginFragment2, true)
.build()
2.Navigation动态加载
除了在xml中设置navGraph,有很多场景下,我们会根据业务场景动态设置一些navGraph,或者某些navGraph是需要动态获取一些参数之后才去初始化的,这时候,就可以使用Navigation的动态加载方案。
首先,需要在androidx.fragment.app.FragmentContainerView 容器中,去掉navGraph的引用,然后在Activity中,动态指定要引用的navGraph,代码如下所示。
// 动态加载
val navHostFragment: NavHostFragment =
supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment
val navigation = navHostFragment.navController.navInflater.inflate(R.navigation.login_navigation)
navigation.setStartDestination(R.id.loginFragment2)
navHostFragment.navController.graph = navigation