一起来学Android Jetpack - Navigation(一)

425 阅读6分钟

一、简介

1.定义

什么是Navigation呢?

Navigation是一个可简化Android导航的库和插件。

更确切的来说,Navigation是用来管理Fragment的切换,并且可以通过可视化的方式,看见App的交互流程。

2.优点

  • 处理fragment的切换。
  • 默认情况下正确处理fragment的前进与后退。
  • 为过渡和动画提供标准化的资源。
  • 可以绑定ToolbarBottomNavigationViewActionBar等。
  • SafeArgs(Gradle插件)数据传递时提供类型安全性。
  • ViewModel支持。
  • 实现和处理深层链接。

3.名词介绍

Navigation中最关键的三要素:

  1. Navigation Graph:只是一个新的资源文件(res下),用户在可视化界面可以看出他能够到达的Destination(屏幕界面),以及流程关系。
  2. NavHostFragment:当前fragment的容器。
  3. 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就是我们的目的地,在此之前,我已经写好了一个LoginFragmentOneFragmentTwoFragmentThreeFragment,添加Destination的操作完成后如下所示:

添加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,也可以是ActivityDialog

第三步 建立NavHostFragment

我们需要创建一个MainActivityactivity_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属性解释:

  1. android:name:值必须是androidx.navigation.fragment.NavHostFragment,代表这个容器就是用来管理Fragment的容器
  2. app:navGraph:存放的是第二步建好导航的资源文件,也就是确定了Navigation Graph
  3. app:defaultNavHost="true":代表可以拦截系统的返回键,用来托管路由

FragmentContainerView是一个特殊的Fragment,只能添加FragmentFragmentContainerView内部会通过反射的方式,初始化名为name所指定的class——NavHostFragment,它就是所有需要管理的FragmentContainer

第四步 界面跳转、参数传递和动画

方式一:利用ID导航

目标:LoginFragment携带keyname的数据跳转到OneFragmentOneFragment接收后显示。
登录按钮的点击事件如下:

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将数据传到TwoFragmentTwoFragment接收后显示。

<?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标签
  1. app:destination:跳转完成到达的fragment的Id
  2. app:popUpTo:将fragment中弹出,直到某个Id的fragment(之后会有讲解)
argument标签
  1. android:name:标签名字
  2. app:argType:标签类型
  3. 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 Argsaction可以由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