crazyandcoder

Android Jetpack教程(1-navigation)

2021.07.19

navigation.png

1 简介

Navigation 是一个导航组件,通过在 xml 中设置导航的开始和目的地,简化了 android 中的导航实现方式。实现 Navigation 有三要素:

1.1 Navigation Graph

这是一个资源文件,用户在可视化界面可以看出他能够到达的 Destination 以及流程关系。从下面这张图可以清楚的看到,fragmentMain 可以跳转到 fragmentViewTransaction,fragmentChooseRecipinple,fragmentViewBalance,在 fragmentChooseRecipinple 中,它又可以跳转到fragmentSpecifyAmount 和 fragmentConfirmation,从 xml 可以很方便的看到跳转起始点。


navigationgraph_2xcallouts.png

1.2 NavHostFragment

NavHostFragment 指的是当前 Fragment 的容器。

1.3 NavController

NavController 是导航的控制者。

1.4 总结

截屏20210720 下午5.53.13.png

2 参考

  1. Google 官方教程 learn-navigation
  2. Google 官方示例 android-navigation

3 特点

  1. 默认情况下正确处理Fragment的前进和后退
  2. 为过渡和动画提供标准化的资源
  3. 实现和处理深层连接
  4. 可以绑定Toolbar、BottomNavigationView和ActionBar等
  5. SafeArgs(Gradle插件) 数据传递时提供类型安全性

4 示例

4.1 依赖导入

dependencies {
  def nav_version = "2.3.5"

  // Java language implementation
  implementation "androidx.navigation:navigation-fragment:$nav_version"
  implementation "androidx.navigation:navigation-ui:$nav_version"

  // Kotlin
  implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
  implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

  // Feature module Support
  implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"

  // Testing Navigation
  androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"

  // Jetpack Compose Integration
  implementation "androidx.navigation:navigation-compose:2.4.0-alpha04"
}

4.2 配置 Navigation Graph

  1. 在 res 目录上面右击选中 New 然后选择 Android Resource File 接着在 Resource type 选中 Navigation 即可。

截屏20210722 上午10.12.51.png

  1. 在 File name 中输入名称如:nav_graph 即可。

截屏20210722 上午10.16.32.png

  1. 点击 OK 即可生成 navigation 资源文件目录了。

截屏20210722 上午10.28.39.png

按照上面的三个步骤,我们创建几个 fragment,代码如下:

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


    <fragment
        android:id="@+id/mainFragment"
        android:name="com.crazyandcoder.learn.jetpack.navigation.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" >
        <action
            android:id="@+id/action_mainFragment_to_viewBalanceFragment"
            app:destination="@id/viewBalanceFragment" />
        <action
            android:id="@+id/action_mainFragment_to_viewTransationsFragment"
            app:destination="@id/viewTransationsFragment" />
        <action
            android:id="@+id/action_mainFragment_to_chooseRecipincleFragment"
            app:destination="@id/chooseRecipincleFragment" />
    </fragment>
    <fragment
        android:id="@+id/viewBalanceFragment"
        android:name="com.crazyandcoder.learn.jetpack.navigation.ViewBalanceFragment"
        android:label="fragment_view_balance"
        tools:layout="@layout/fragment_view_balance" />
    <fragment
        android:id="@+id/viewTransationsFragment"
        android:name="com.crazyandcoder.learn.jetpack.navigation.ViewTransationsFragment"
        android:label="fragment_view_transations"
        tools:layout="@layout/fragment_view_transations" />
    <fragment
        android:id="@+id/chooseRecipincleFragment"
        android:name="com.crazyandcoder.learn.jetpack.navigation.ChooseRecipincleFragment"
        android:label="fragment_choose_recipincle"
        tools:layout="@layout/fragment_choose_recipincle" >
        <action
            android:id="@+id/action_chooseRecipincleFragment_to_specifyAmountFragment"
            app:destination="@id/specifyAmountFragment" />
    </fragment>
    <fragment
        android:id="@+id/specifyAmountFragment"
        android:name="com.crazyandcoder.learn.jetpack.navigation.SpecifyAmountFragment"
        android:label="fragment_specify_amount"
        tools:layout="@layout/fragment_specify_amount" >
        <action
            android:id="@+id/action_specifyAmountFragment_to_confirmationFragment"
            app:destination="@id/confirmationFragment" />
    </fragment>
    <fragment
        android:id="@+id/confirmationFragment"
        android:name="com.crazyandcoder.learn.jetpack.navigation.ConfirmationFragment"
        android:label="fragment_confirmation"
        tools:layout="@layout/fragment_confirmation" />
</navigation>

以上代码中有几个标签需要注意:

  1. navigation:根标签,通过 startDestination 配置指定默认的启动页面。
  2. fragment: fragment 标签代表一个 fragment 视图。
  3. action:action 标签定义了页面跳转的行为,destination 标签定义跳转的目标页,跳转时还可以定义跳转动画。

4.3 设置 NavHostFragment

NavHostFragment 简单来讲就是一个导航界面容器,用来展示导航中一系列的 Fragment

<?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=".NavigationMainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph" />


</androidx.constraintlayout.widget.ConstraintLayout>
  1. android:name 属性包含 NavHost 实现的类名称。
  2. app:navGraph 属性将 NavHostFragment 与导航图相关联。导航图会在此 NavHostFragment 中指定用户可以导航到的所有目的地。
  3. app:defaultNavHost="true" 属性确保您的 NavHostFragment 会拦截系统返回按钮。请注意,只能有一个默认 NavHost。

经过上面步骤我们基本上就设定好了基本配置,接着我们就可以通过 NavController 控制 fragment 的切换了。

4.4 设置 NavController

每个 NavHost 均有自己的相应 NavController。可以通过 findNavController 来获取 NavController,然后使用 NavController 的 navigate 或者 navigateUp 方法来进行页面之间的路由操作。

在 MainFragment 中使用:

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        var view = inflater.inflate(R.layout.fragment_main, container, false)
        view.findViewById<Button>(R.id.view_transations).setOnClickListener {
            Navigation.findNavController(it)
                .navigate(R.id.action_mainFragment_to_viewTransationsFragment)
        }

        view.findViewById<Button>(R.id.send_money).setOnClickListener {
            Navigation.findNavController(it)
                .navigate(R.id.action_mainFragment_to_chooseRecipincleFragment)
        }

        view.findViewById<Button>(R.id.view_balance).setOnClickListener {
            Navigation.findNavController(it)
                .navigate(R.id.action_mainFragment_to_viewBalanceFragment)
        }
        return view
    }

效果如下:

SVID_20210722_113351_1.gif

4.5 页面传值

Fragment 的切换经常伴随着参数的传递,为了配合 Navigation 组件在切换 Fragment 时传递参数,提供了 Safe Args 和 Bundle 两种参数传递方式。

4.5.1 Bundle 传递数据

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        var view = inflater.inflate(R.layout.fragment_main, container, false)
        view.findViewById<Button>(R.id.view_transations).setOnClickListener {
	    Bundle bundle = new Bundle();
            bundle.putString("key", "from MainFragment");
            Navigation.findNavController(it)
                .navigate(R.id.action_mainFragment_to_viewTransationsFragment,bundle)
        }
        return view
    }

在目的地 Fragment 中通过如下代码获取传值:

String keyStr = getArguments().getString("key");

4.5.2 Safe Args 传递数据

首先,在 Project 的 build.gradle 中添加 classpath 配置,如下所示:

def nav_version = "2.3.2"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"

然后,在 app 的 build.gradle 添加 apply plugin 脚本,如下所示:

apply plugin: 'androidx.navigation.safeargs'

首先我们需要在 nav_graph.xml 中添加上我们自定义的参数,假如我们打算从 MainFragment 中把参数传递给 ChooseRecipincleFragment,那么我们则需要在 ChooseRecipincleFragment(接收目的地)的 标签下添加上 标签并添加上名字、默认值、类型三种属性。代码如下所示:

    <fragment
        android:id="@+id/chooseRecipincleFragment"
        android:name="com.crazyandcoder.learn.jetpack.navigation.ChooseRecipincleFragment"
        android:label="fragment_choose_recipincle"
        tools:layout="@layout/fragment_choose_recipincle" >
        <action
            android:id="@+id/action_chooseRecipincleFragment_to_specifyAmountFragment"
            app:destination="@id/specifyAmountFragment" />
        <argument
            android:name="from"
            app:argType="string"
            android:defaultValue="defaultValue" />
    </fragment>

配置完成后记重新 rebuild 下项目,会生成 /build/generated/source/navigation-args///Dircetions,如下所示:

截屏20210722 下午12.03.35.png

在刚刚 Rebuild 之后发送方 MainFragment 生成了 MainFragmentDirections 类,接收方生成了 ChooseRecipincleFragmentArgs 类,这里面有我们需要传递参数的方法, 接下来我们看下如何在代码中传递安全的数据。代码如下所示:

在 MainFragment 中:

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        var view = inflater.inflate(R.layout.fragment_main, container, false)

        view.findViewById<Button>(R.id.send_money).setOnClickListener {

            var action = MainFragmentDirections.actionMainFragmentToChooseRecipincleFragment()
                .setFrom("mainFragment")

            Navigation.findNavController(it).navigate(action)

        }
        return view
    }

在接收方 ChooseRecipincleFragment 中:

    private var from = "default"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            from = ChooseRecipincleFragmentArgs.fromBundle(it).from
        }
    }

接收到 MainFragment 传递过来的 mainFragment 值,效果如下:

截屏20210722 下午12.11.21.png

5 总结

以上便是 jetpack 中关于 navigation 的使用方法,详细的 demo 可以查看 learn-jetpack-navigation