Jetpack系列——Navigation

1,136 阅读5分钟

在日常开发,尤其是大型项目中,越来越多地使用Activity嵌套多个Fragment的UI模式开发,但对于Fragment的生命周期、隐藏显示、动画控制和切换传值等都是比较麻烦的一件事。通常做法是通过FragmentManager来管理,这种方式代码冗余,不易维护。

Jetpack为此提供了Navigation组件,用来帮助开发者更高效简单地开发和维护Fragment。Navigation组件可以类似xml布局一样可视化编程,开发更加简单便捷,提供了一些属性来实现Fragment之间的跳转等操作,还提供了Fragment之间跳转的动画和传参的能力,除此之外还支持deeplink。

Navigation主要组成

  • Navigation Graph:一种新的xml文件,内部定义Fragment及他们之间的关系;
  • NavHostFragment:一种特殊的Fragment,根据名称可以看出是一种Fragment的容器,Navigation Graph中定义的Fragment正是通过NavHostFragment来展示;
  • NavController:是一个Java类,用来实现Fragment之间的跳转等操作。

使用

1、创建Navigation Graph

Navigation Graph是一种xml文件,也属于res资源的一种,所以新建Navigation Graph的时候需要在res目录下操作:

res目录下点击New,选择Android Resource,Resource Type选择Navigation,为文件命名,点击ok就可以创建一个Navigation Graph文件。创建好文件后打开如下图 :

2、创建NavHostFragment

我们知道,使用Navigation来操作Fragment,需要为它们定义一个容器Fragment,叫做NavHostFragment,它是Jetpack提供的一个类,我们在Activity的xml布局中直接使用就可以,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NavigationActivity">

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

</androidx.constraintlayout.widget.ConstraintLayout>

这里要注意的是,我们需要在Activity的xml布局中添加fragment,在该元素下添加name属性,并指定是android:name="androidx.navigation.fragment.NavHostFragment",将defaultNavHost属性设置成true,则该Fragment会默认作为NavHostFragment,navGraph属性用来执行其所对应的Navigation Graph文件。

这是再打开之前创建好的Navigation Graph文件就会发现,Destinations面板上的HOST选项已经对应设置了我们在Activity的xml中声明的内容:

3、创建destination

在Navigation Graph中,点就如图中的+号,选择create new destination,选择一个Blank Fragment并创建,如图:

destination指的是目标Fragment,即下一个Fragment,创建好之后便可以在destination面板中的GRAPH选项中看到定义好的Fragment:

不难发现,在navigationOneFragment后面有个"start",表示的是起始Fragment,即第一个展示的Fragment。此时再看下Navigation Graph的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_graph"
    app:startDestination="@id/navigationOneFragment">

    <fragment
        android:id="@+id/navigationOneFragment"
        android:name="com.jia.demo.NavigationOneFragment"
        android:label="fragment_navigation_one"
        tools:layout="@layout/fragment_navigation_one" />

</navigation>

最外层的navigation节点中有一个属性:startDestination,指定了我们创建好的Fragment。

4、切换Fragment

接着用同样的方式创建一个NavigationTwoFragment,选中NavigationOneFragment,选择右侧圆形按钮,拖拽至NavigationTwoFragment,如图:

再查看Graph文件:

<?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_graph"
    app:startDestination="@id/navigationOneFragment">

    <fragment
        android:id="@+id/navigationOneFragment"
        android:name="com.jia.demo.NavigationOneFragment"
        android:label="fragment_navigation_one"
        tools:layout="@layout/fragment_navigation_one" >
        <action
            android:id="@+id/action_navigationOneFragment_to_navigationTwoFragment"
            app:destination="@id/navigationTwoFragment" />
    </fragment>
    
    <fragment
        android:id="@+id/navigationTwoFragment"
        android:name="com.jia.demo.NavigationTwoFragment"
        android:label="fragment_navigation_two"
        tools:layout="@layout/fragment_navigation_two" />
</navigation>

这时NavigationOneFragment节点中多了一个action子节点,其中声明了destination属性,指向navigationTwoFragment,即表示NavigationOneFragment的下一个页面就是NavigationTwoFragment。

接下来就该使用之前提到过的NavController了。

完善一下NavigationOneFragment的布局,增加一个Button,并设置点击事件:

private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"


class NavigationOneFragment : Fragment() {

    private var param1: String? = null
    private var param2: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_navigation_one, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        button_jump.setOnClickListener {
            Navigation.findNavController(it)
                .navigate(R.id.action_navigationOneFragment_to_navigationTwoFragment)
        }
        
//   button_jump.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_navigationOneFragment_to_navigationTwoFragment))
    }

    companion object {

        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            NavigationOneFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

除了使用Navigation.findNavController(view).navigate()的方式,还可以使用:Navigation.createNavigateOnClickListener()的方式直接创建一个OnClickListener。传入的id就是Graph文件中的action节点的id。

这样就可以创建Fragment并实现Fragment切换了。

5、为Fragment切换设置动画

为Fragment的切换添加动画,那就需要在anim文件夹下先定义好动画了,不再详细描述。

在Graph的Destinations面板中,选中跳转的箭头,右侧的Attributes面板中显示Animations的区域,如图:

为了方便,我这里直接使用了默认的动画,再看下graph文件:

<?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_graph"
    app:startDestination="@id/navigationOneFragment">

    <fragment
        android:id="@+id/navigationOneFragment"
        android:name="com.jia.demo.NavigationOneFragment"
        android:label="fragment_navigation_one"
        tools:layout="@layout/fragment_navigation_one" >
        <action
            android:id="@+id/action_navigationOneFragment_to_navigationTwoFragment"
            app:destination="@id/navigationTwoFragment"
            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/navigationTwoFragment"
        android:name="com.jia.demo.NavigationTwoFragment"
        android:label="fragment_navigation_two"
        tools:layout="@layout/fragment_navigation_two" />
</navigation>

在action节点中多了四个属性,分别对应各个动画,这样就为Fragment切换增加了动画。

除了在Graph文件中进行可视化设置动画,也可以通过代码来设置:

       button_jump.setOnClickListener {
            val options = NavOptions.Builder()
                .setEnterAnim(R.anim.nav_default_enter_anim)
                .setExitAnim(R.anim.nav_default_exit_anim)
                .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
                .setPopExitAnim(R.anim.nav_default_pop_exit_anim)
                .build()
            Navigation.findNavController(it)
                .navigate(R.id.action_navigationOneFragment_to_navigationTwoFragment, null, options)
        }

创建NavOptions对象,设置各个动画,在navigate方法中传入即可,该方法第二个参数为Buddle对象,后面会介绍。

在Fragment中传递参数

1、普通的传参方式

我们最长使用的就是通过Bundle来实现在Fragment之间传递数据,NavController提供了重载的几个navigate方法,可以传入Bundle对象:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        button_jump.setOnClickListener {
            val options = NavOptions.Builder()
                .setEnterAnim(R.anim.nav_default_enter_anim)
                .setExitAnim(R.anim.nav_default_exit_anim)
                .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
                .setPopExitAnim(R.anim.nav_default_pop_exit_anim)
                .build()

            var bundle = Bundle()
            bundle.putString("key", "value")

            Navigation.findNavController(it).navigate(
                R.id.action_navigationOneFragment_to_navigationTwoFragment,
                bundle,
                options
            )
        }
    }

接收的时候在目标Fragment中:

private const val KEY = "key"

class NavigationTwoFragment : Fragment() {
    private var value: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            value = it.getString(KEY)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_navigation_two, container, false)
    }
}

2、使用safe args传递参数

使用safe args,首先需要添加依赖和插件,在project的build.gradle文件中:

dependencies {
      classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0"
}

在module的build.gradle文件中:

apply plugin: 'androidx.navigation.safeargs'

在Graph文件的fragment节点中,和action同级添加argument节点:

<?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_graph"
    app:startDestination="@id/navigationOneFragment">

    <fragment
        android:id="@+id/navigationOneFragment"
        android:name="com.jia.demo.NavigationOneFragment"
        android:label="fragment_navigation_one"
        tools:layout="@layout/fragment_navigation_one">
        <action
            android:id="@+id/action_navigationOneFragment_to_navigationTwoFragment"
            app:destination="@id/navigationTwoFragment"
            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" />

        <argument
            android:name="params"
            android:defaultValue="default_value"
            app:argType="string" />
    </fragment>

    <fragment
        android:id="@+id/navigationTwoFragment"
        android:name="com.jia.demo.NavigationTwoFragment"
        android:label="fragment_navigation_two"
        tools:layout="@layout/fragment_navigation_two" />
</navigation>

添加arguments节点之后,便会自动生成java代码:

传递参数:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        button_jump.setOnClickListener {
            val options = NavOptions.Builder()
                .setEnterAnim(R.anim.nav_default_enter_anim)
                .setExitAnim(R.anim.nav_default_exit_anim)
                .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
                .setPopExitAnim(R.anim.nav_default_pop_exit_anim)
                .build()

            var bundle = NavigationOneFragmentArgs.Builder()
                .setParams("one param")
                .build()
                .toBundle()

            Navigation.findNavController(it).navigate(
                R.id.action_navigationOneFragment_to_navigationTwoFragment,
                bundle,
                options
            )
        }
    }

通过自动生成的NavArgs类,使用builder模式创建出Bundle对象,使用之前介绍的方式,便可以在Fragment切换时传递参数。接收的时候也类似:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        var params = arguments?.let { NavigationOneFragmentArgs.fromBundle(it).params}
        textView.text = params
    }

safe args顾名思义就是安全的参数,使用更加方便也更加安全。

Jetpack为开发者提供了Navigation组件,使用该组件可以方便、快捷、可视化地实现Fragment之间的切换以及动画和传值的功能。