Jetpack(五)—— Navigation

Navigation

1 简介

对于单个Activity嵌套多个FragmentUI架构方式,Fragment的管理一直是一个比较麻烦的事情,需要通过FragmentManagerFragmentTransaction来管理Fragment之间的切换,这其中还包括了对应用程序的App Bar的管理、Fragment间的切换动画、Fragment间的参数传递。总之,使用起来不是特别友好。

NavigaionAndroid JetPack框架中的一员,是一套新的Fragment管理框架。可以帮助开发者很好的处理Fragment之间的跳转,优雅的支持Fragment之间的转场动画,支持通过deeplink直接定位到Fragment, 通过第三方的插件支持Fragment之间安全的参数传递,可视化的编辑各个组件之间的跳转关系。

导航组件的推出,使得我们在搭架应用架构的时候,可以考虑一个功能模块就是一个Activity,模块中每个子页面使用Fragment实现,使用Navigation处理Fragment之间的导航。更有甚者,设计一个单Activity的应用也不是没有可能(这完美的契合了Jake Wharton大神单Activity的建议。)。最后还要提一点,Navigation不只是能管理Fragment,它还支持Activity,小伙伴们请注意这一点。

Navigation是一个可视化Android导航的库和插件。更确切的来说, Navigation是用来管理Fragment的切换,并且可以通过可视化的方式,看见APP的交互流程。

Jetpack(五)—— Navigation_第1张图片

优点:

  • 可视化的页面导航图,可以编辑各个组件之间的跳转关系
  • 优雅的支持Fragment之间的转场动画
  • 通过第三方的插件(SafeArgsGradle插件))支持Fragment之间类型安全的参数传递
  • 通过NavigationUI类,对菜单,底部导航,抽屉菜单导航进行方便统一的管理
  • 支持通过deeplink直接定位到Fragment

2 Navigation的核心要素

Jetpack(五)—— Navigation_第2张图片

graph [ɡræf] 图表,曲线图;坐标图

Navigation GraphNavigation的配置文件,位于res/navigation/目录下的xml文件。这个文件是对导航中各个组件的跳转关系的预览。Design模式下,可以很清晰的看到组件之间关系,如图所示:

Jetpack(五)—— Navigation_第3张图片
NavHost:一个空白的父容器,承担展示Fragment的作用,源码中父容器的实现是NavHostFragment。在Activity中引入这个fragment才能使用Navigation

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/nav_host_fragment_login"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/login_navigation" />

LinearLayout>

NavController:导航组件的跳转控制器,管理导航的对象,控制NavHost中目标页面的展示。

3 使用

第一步:添加依赖

在模块层的build.gradle中添加:

dependencies {
		...
    implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
    implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
}

第二步:创建navigation导航

res文件夹下创建navigation目录,然后在这个新建的navigation目录下创建一个Navigation Resource File文件,如图所示:

Jetpack(五)—— Navigation_第4张图片

Jetpack(五)—— Navigation_第5张图片

创建成功后,添加Destination,并创建跳转行为,如下所示:

Jetpack(五)—— Navigation_第6张图片

代码如下所示:

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/login_navigation"
    app:startDestination="@id/welcome">

    <fragment
        android:id="@+id/welcome"
        android:name="com.example.kotlintest.WelcomeFragment"
        android:label="WelcomeFragment">
        <action
            android:id="@+id/action_welcomeFragment_to_loginFragment"
            app:destination="@id/login" />
        <action
            android:id="@+id/action_welcomeFragment_to_registerFragment"
            app:destination="@id/register" />
        
    fragment>
    <fragment
        android:id="@+id/login"
        android:name="com.example.kotlintest.LoginFragment"
        android:label="LoginFragment" />
    <fragment
        android:id="@+id/register"
        android:name="com.example.kotlintest.RegisterFragment"
        android:label="RegisterFragment" />
navigation>

navigation标签的属性:

navigation标签的属性

第三步:建立NavHostFragment

创建一个LoginActivity ,在activity_login.xml文件中:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <fragment
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/login_navigation" />

LinearLayout>

NavHostFragment的属性:

Jetpack(五)—— Navigation_第7张图片

app:defaultNavHost置为true,是让当前的导航容器NavHostFragment处理系统返回键,在Navigation容器中如果有页面的跳转,点击返回按钮会先处理容器中Fragment页面间的返回,如果当栈中只剩一个页面的时候,系统返回键将由当前Activity处理。如果值为false则直接处理Activity页面的返回。

第四步:页面切换,传递参数

WelcomeFragment中,点击登录和注册按钮可以分别跳转到LoginFragmentRegisterFragment中。

Jetpack(五)—— Navigation_第8张图片

普通方式

只要调用Fragment.findNavController().navigate(resId)方法就可以实现页面切换,resId标签的id,代码如下所示:

btnLogin.setOnClickListener {
  findNavController().navigate(R.id.action_welcomeFragment_to_loginFragment)
}

另外,还可以添加页面切换动画效果,在res/anim目录下添加以下文件:

slide_in_left

<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_mediumAnimTime"
    android:fromXDelta="-100.0%p"
    android:toXDelta="0.0" />

slide_in_right

<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_mediumAnimTime"
    android:fromXDelta="100.0%p"
    android:toXDelta="0.0" />

slide_out_left

<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_mediumAnimTime"
    android:fromXDelta="0.0"
    android:toXDelta="100.0%p" />

slide_out_right

<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_mediumAnimTime"
    android:fromXDelta="0.0"
    android:toXDelta="100.0%p" />

代码如下所示:

btnLogin.setOnClickListener {
  val navOption = navOptions {
    anim {
      enter = R.anim.slide_in_right
      exit = R.anim.slide_out_left
      popEnter = R.anim.slide_in_left
      popExit = R.anim.slide_out_right
    }
  }

  findNavController().navigate(
    R.id.action_welcomeFragment_to_loginFragment,
    null, // 传递参数
    navOption
  )
}

或者在login_navigationDesign中设置,如下所示:

Jetpack(五)—— Navigation_第9张图片

Jetpack(五)—— Navigation_第10张图片

login_navigation中代码如下所示:

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/login_navigation"
    app:startDestination="@id/welcome">

    <fragment
        android:id="@+id/welcome"
        android:name="com.example.kotlintest.WelcomeFragment"
        android:label="WelcomeFragment">
        <action
            android:id="@+id/action_welcomeFragment_to_loginFragment"
            app:destination="@id/login"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
        <action
            android:id="@+id/action_welcomeFragment_to_registerFragment"
            app:destination="@id/register" />
    fragment>
    <fragment
        android:id="@+id/login"
        android:name="com.example.kotlintest.LoginFragment"
        android:label="LoginFragment" />
    <fragment
        android:id="@+id/register"
        android:name="com.example.kotlintest.RegisterFragment"
        android:label="RegisterFragment" />
navigation>

如下图所示:

Jetpack(五)—— Navigation_第11张图片

传递参数(不推荐方式):

btnLogin.setOnClickListener {
  val bundle = Bundle()
  bundle.putString("name", "萧峰")
  bundle.putInt("age", 31)
  findNavController().navigate(R.id.action_welcomeFragment_to_loginFragment, bundle)
}

获取参数:

Log.e("LoginFragment", "onResume: ${arguments?.get("name")}")
Log.e("LoginFragment", "onResume: ${arguments?.get("age")}")
使用safe args传递参数(推荐使用)

如果需要使用SafeArgs插件,还要在项目的build.gradle中添加:

buildscript {
    ...
    dependencies {
				...
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"

    }
}

以及模块层的build.gradle文件添加:

plugins {
    ...
    id 'kotlin-android-extensions'
    id 'androidx.navigation.safeargs.kotlin'
}

配置完成以后记得要clean rebuild 一下。会生成 {module}/build/generated/source/navigation-args/{debug}/{packaged}/{Fragment}Dircetions这样一个文件。其中的文件名后缀为Directions

Jetpack(五)—— Navigation_第12张图片

Design 模式下,点击Arguments,添加参数,如下所示:

Jetpack(五)—— Navigation_第13张图片

Jetpack(五)—— Navigation_第14张图片

代码如下:


<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/login_navigation"
    app:startDestination="@id/welcome">

    <fragment
        android:id="@+id/welcome"
        android:name="com.example.kotlintest.WelcomeFragment"
        android:label="WelcomeFragment">
        <action
            android:id="@+id/action_welcomeFragment_to_loginFragment"
            app:destination="@id/login"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
        <action
            android:id="@+id/action_welcomeFragment_to_registerFragment"
            app:destination="@id/register" />
        <argument
            android:name="name"
            app:argType="string"
            app:nullable="true"
            android:defaultValue="I am listening" />
        <argument
            android:name="age"
            app:argType="integer"
            android:defaultValue="30" />
    fragment>
    <fragment
        android:id="@+id/login"
        android:name="com.example.kotlintest.LoginFragment"
        android:label="LoginFragment" />
    <fragment
        android:id="@+id/register"
        android:name="com.example.kotlintest.RegisterFragment"
        android:label="RegisterFragment" />
navigation>

argument标签:

Jetpack(五)—— Navigation_第15张图片

代码如下所示:

btnLogin.setOnClickListener {
  val bundle = WelcomeFragmentArgs("萧峰", 31).toBundle()
  findNavController().navigate(R.id.action_welcomeFragment_to_loginFragment, bundle)
}

获取参数:

// 方式1:使用fromBundle
val bundle = arguments
if (bundle != null) {
  val welcomeArgs = WelcomeFragmentArgs.fromBundle(bundle)
  Log.e("LoginFragment", "onResume: ${welcomeArgs.name}")
  Log.e("LoginFragment", "onResume: ${welcomeArgs.age}")
}

// 方式2:使用委托的方式
val args by navArgs<WelcomeFragmentArgs>()
Log.e("LoginFragment", "onResume: ${args.name}")
Log.e("LoginFragment", "onResume: ${args.age}")

4 导航堆栈管理

Navigation有自己的任务栈,每次调用navigate函数,都是一个入栈操作,出栈操作有以下几种方式,下面详细介绍几种出栈方式和使用场景。

4.1 系统返回键

通过代码来测试,以下是stack_navigation

Jetpack(五)—— Navigation_第16张图片

代码如下所示:

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/stack_navigation"
    app:startDestination="@id/AFragment">

    <fragment
        android:id="@+id/AFragment"
        android:name="com.example.kotlintest.AFragment"
        android:label="AFragment" >
        <action
            android:id="@+id/action_AFragment_to_BFragment"
            app:destination="@id/BFragment" />
    fragment>
    <fragment
        android:id="@+id/BFragment"
        android:name="com.example.kotlintest.BFragment"
        android:label="BFragment" >
        <action
            android:id="@+id/action_BFragment_to_CFragment"
            app:destination="@id/CFragment" />
    fragment>
    <fragment
        android:id="@+id/CFragment"
        android:name="com.example.kotlintest.CFragment"
        android:label="CFragment" />
navigation>

activity_stack的代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/nav_host_fragment_stack"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/stack_navigation" />

LinearLayout>

xml中配置app:defaultNavHost="true",才能让导航容器拦截系统返回键,点击系统返回键,是默认的出栈操作,回退到上一个导航页面。如果当栈中只剩一个页面的时候,系统返回键将由当前Activity处理。

运行程序:

xml中配置app:defaultNavHost="false",系统返回键将由当前Activity处理:

Jetpack(五)—— Navigation_第17张图片

4.2 自定义返回键

如果页面上有返回按钮,可以调用popBackStack()或者navigateUp()返回到上一个页面。 以下是源码:

public class NavController {
  public boolean navigateUp() {
    if (getDestinationCountOnBackStack() == 1) {
      // If there's only one entry, then we've deep linked into a specific destination
      // on another task so we need to find the parent and start our task from there
      NavDestination currentDestination = getCurrentDestination();
      int destId = currentDestination.getId();
      NavGraph parent = currentDestination.getParent();
      while (parent != null) {
        if (parent.getStartDestination() != destId) {
          ...
          return true;
        }
        destId = parent.getId();
        parent = parent.getParent();
      }
      // We're already at the startDestination of the graph so there's no 'Up' to go to
      return false;
    } else {
      return popBackStack();
    }
  }

  public boolean popBackStack() {
    if (mBackStack.isEmpty()) {
      // Nothing to pop if the back stack is empty
      return false;
    }
    // Pop just the current destination off the stack
    return popBackStack(getCurrentDestination().getId(), true);
  }

}

从源码可以看出,当栈中任务大于1个的时候,两个函数没什么区别。当栈中只有一个导航首页(start destination)的时候,navigateUp()不会弹出导航首页,直接返回falsepopBackStack则会把导航首页也出栈,但是由于没有回退到任何其他页面,此时popBackStack会返回false, 如果此时又继续调用navigate函数,会发生exception

navigateUp

Jetpack(五)—— Navigation_第18张图片

popBackStack:

所以google官网说不建议把导航首页也出栈。如果导航首页出栈了,此时需要关闭当前Activity,或者跳转到其他导航页面。示例代码如下:

if (!findNavController().popBackStack()) {
  activity?.finish()
}

4.3 popUpTopopUpToInclusive

假设有ABC 3个页面,跳转顺序是A to BB to CC to A。依次执行几次跳转后,栈中的顺序是A->B->C->A->B->C->A。此时如果用户按返回键,会发现反复出现重复的页面:

此时用户的预期应该是在A页面点击返回,应该退出应用。

在关联的元素中添加app:popUpTo属性可以将除目标目的地外的其他目的地都出栈。设置app:popUpToInclusive="true",以表明在app:popUpTo中指定的目的地也应从返回堆栈中移除。

此时就需要在CAaction中设置popUpTo = "@id/AFragment"。这样在C跳转A的过程中会把BC出 栈。但是还会保留上一个A的实例,加上新创建的这个A的实例,就会出现2A的实例,因此还需要设置popUpToInclusive = true。这个配置会把上一个页面的实例也弹出栈,只保留新建的实例:

inclusive 包容广阔的;包括一切费用在内的;包括的,包含的;无性别歧视的;对外开放的

5 其他

Navigation可以绑定menusdrawersbottom navigation,这里以bottom navigation为例,先在navigation目录下新创建了 main_navigation.xml,接着新建了MainActivity ,下面则是activity_main.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <fragment
        android:id="@+id/nav_host_fragment_activity_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:defaultNavHost="true"
        app:navGraph="@navigation/main_navigation" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/menu" />

LinearLayout>

以下是main_navigation中的代码:

<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/main_navigation"
    app:startDestination="@id/navigation_welcome">

    <fragment
        android:id="@+id/navigation_welcome"
        android:name="com.example.kotlintest.WelcomeFragment"
        android:label="WelcomeFragment"
        tools:layout="@layout/welcome_fragment" />
    <fragment
        android:id="@+id/navigation_login"
        android:name="com.example.kotlintest.LoginFragment"
        android:label="LoginFragment"
        tools:layout="@layout/login_fragment" />
    <fragment
        android:id="@+id/navigation_register"
        android:name="com.example.kotlintest.RegisterFragment"
        android:label="RegisterFragment"
        tools:layout="@layout/register_fragment" />
navigation>

以下是menu代码:

<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/navigation_welcome"
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/main_welcome" />

    <item
        android:id="@+id/navigation_login"
        android:icon="@drawable/ic_dashboard_black_24dp"
        android:title="@string/main_login" />

    <item
        android:id="@+id/navigation_register"
        android:icon="@drawable/ic_notifications_black_24dp"
        android:title="@string/main_register" />

menu>

以下是MainActivity的代码:

class MainActivity : BaseActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val navController = findNavController(R.id.nav_host_fragment_activity_main)
    val appBarConfiguration = AppBarConfiguration(
      setOf(
        R.id.navigation_welcome, R.id.navigation_login, R.id.navigation_register
      )
    )

    setupActionBarWithNavController(navController, appBarConfiguration)
    nav_view.setupWithNavController(navController)

  }
}

运行如下:

参考

https://juejin.cn/post/6906689261441908743#heading-9
https://blog.csdn.net/nanquan11/article/details/109807501
https://blog.csdn.net/qq_40638618/article/details/121476856
https://zhuanlan.zhihu.com/p/69562454
https://www.jianshu.com/p/f7101e2d897f

你可能感兴趣的:(android,gradle,android,studio)