Android导航组件学习(四)

289 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

概述

通过之前的学习,我们已经对导航组件有了充分的认识,并且已经能够根据实际项目的需要来设计符合我们需要的导航图。同时,在学习之前的内容的时候,我们也涉及到了简单的导航图的跳转,在定义好导航图和操作之后,我们可以使用NavControllernavigate()并向其中传递操作id来实现目的地之间的跳转。

使用id导航

在之前的学习中,我们一直是使用actionid来执行目的地之间的跳转,其实我们也可以使用fragmentid来执行页面的跳转,下面的代码定义了一个Fragment,然后分别使用actionfragmentid来执行跳转,如下所示:

    <fragment
            android:id="@+id/nav_with_id"
            android:name="com.zyf.navstudy.nav_go.id.NavWithIdFragment"
            android:label="@string/nav_with_id"
            tools:layout="@layout/fragment_nav_with_id"
            >

        <action
                android:id="@+id/to_nav_action_or_fragment_id"
                app:destination="@id/nav_with_action_or_fragment_id"
                />

    </fragment>

    <!--根据actionId或者fragmentId跳转的而页面-->
    <fragment
            android:id="@+id/nav_with_action_or_fragment_id"
            android:name="com.zyf.navstudy.nav_go.id.NavWithActionOrFragmentIdFragment"
            android:label="nav with action or fragment id"
            tools:layout="@layout/fragment_nav_with_action_or_fragment_id"
            >
        <argument
                android:name="type"
                app:argType="string"
                />
    </fragment>

从上面定义的导航图中可以看出,我们定义了一个NavWithActionOrFragmentIdFragment,这个页面的布局非常简单,只有一个TextView用于显示是通过哪种方式跳转到当前页面的。这个fragment定义了一个参数,参数名为type,我们在跳转的时候设置这个参数的值,如果是通过actionId(to_nav_action_or_fragment_id)跳转设置为0,如果是通过fragmentId(nav_with_action_or_fragment_id)则设置为1,跳转的代码如下所示:

//action的id
findNavController().navigate(R.id.to_nav_action_or_fragment_id, bundleOf("type" to "0"))
//fragment的id
findNavController().navigate(R.id.nav_with_action_or_fragment_id,bundleOf("type" to "1"))

上面的代码执行完成后的效果如下:

通过id跳转到目的地

从上面的动图可以看出:无论是使用actionId还是使用fragmentId,均可以导航到指定的目的地。

需要注意的是:虽然我们确实可以通过fragmentId来进行目的地之间的导航,但是仍然建议使用actionId来进行这个操作,主要是因为action可以配置更多的参数,虽然我们现在只使用到了这两个参数,但是后面将会学习针对action其它参数的配置。

为按钮直接创建导航

在很多时候,我们导航的触发时机都是通过点击一个按钮实现的,导航组件为这种操作提供了更加便捷的实现方式,我们可以在按钮设置点击事件的时候直接和导航相关的点击操作,模板为:

view.setOnClickListener(Navigation.createNavigateOnClickListener(导航id,携带的参数))

使用这种方式,我们上面的通过fragmentId跳转的代码就可以写成如下的形式:

        binding.btnNavWithFragmentId.setOnClickListener(
            Navigation.createNavigateOnClickListener(
                R.id.nav_with_action_or_fragment_id,
                bundleOf("type" to "1")
            )
        )

导航选项

在上面的学习中,我们使用导航的时候主要关注的还是如何设计导航图,如何配置导航的操作,而针对操作,我们一开始知识简单配置了它的id和目的地id,其实这里还可以配置很多参数,比如导航时使用的动画,弹出行为以及是否应该在单一顶级模式下启动的目的地。下面的代码在之前的基础上演示了相应的情况,同时对action执行了更多的配置:

    <fragment
            android:id="@+id/nav_with_action_or_fragment_id"
            android:name="com.zyf.navstudy.nav_go.id.NavWithActionOrFragmentIdFragment"
            android:label="nav with action or fragment id"
            tools:layout="@layout/fragment_nav_with_action_or_fragment_id"
            >
        <argument
                android:name="type"
                app:argType="string"
                />
        <action android:id="@+id/to_nav_with_id_second"
                app:destination="@id/nav_with_id"
                app:enterAnim="@anim/anim_fragment_enter"
                app:exitAnim="@anim/anim_fragment_exit"
                app:popEnterAnim="@anim/anim_fragment_pop_enter"
                app:popExitAnim="@anim/anim_fragment_pop_exit"
                app:popUpTo="@id/nav_with_id"
                app:popUpToInclusive="true"
                app:popUpToSaveState="false"
                />
    </fragment>

在上面的代码中,我们创建了一个新的action,对这个action添加了更多的参数,这些参数的意义如下:

  • id: 标记当前操作的id
  • destination: 标记需要导航到的目的地的id
  • enterAnim: 进入导航目的地时使用的动画资源(目的地进入默认就使用这里的资源)
  • exitAnim: 退出导航目的地时使用的动画资源(这里配置的资源并没有发现在哪里使用)
  • popEnterAnim: 添加到导航栈时使用的动画资源(当有目的地被退出时重新进入使用这里的资源)
  • popExitAnim: 退出导航栈时使用的动画资源(按返回键是就是使用这里的资源)
  • popUpTo: 指定在进入到destination这个导航目的地时需要退出的导航目的地的起点,这个属性和下面的属性配合使用
  • popUpToInclusive: 当这个属性设置为true时,popUpTo指定的目的地到当前目的地均会退出栈,为false时popUpTo指定的目的地不会退出,popUpTo指向的目的地到当前目的地之间的目的地均会退出
  • popUpToSaveState: 这里属性指定配置了popUpId之后被退出栈的目的地的状态是否会被保存,以便于下次能够快速初始化相关的目的地,需要注意的是,当我们指定了popUpId属性时这个属性默认就为true

针对popUpToInclusive这个属性的说明如下:

假如我们的导航路径是"A -> B -> C -> D",当我们从C导航到D的时候设置了popUpTo为A,这个时候就会出现两种情况:

  • 如果popUpToInclusive设置为true,那么A,B,C三个目的地均会被退出导航栈
  • 如果popUpToInclusive设置为false,那么只有B,C两个目的地会被退出导航栈

下面的动图对比了这两个属性值的区别,如下所示:

当popUpToInclusive设置为true时当popUpToInclusive设置为false时

可以看到,使用这个属性能够很方便地帮助我们控制页面的流转,这样的控制对于诸如用户注册流程来说是十分方便的,一般我们的注册流程都会包含多个页面,当用户最终成功注册以后我们会将注册流程中所有的页面全都从导航栈中退出,这样用户就不会在注册成功以后按返回键还能看到注册的页面了。

另外需要注意的是:使用这里的功能就必须通过actionId去执行导航,否则则不会生效。

返回堆栈

返回堆栈是通过系统去维护的,这里面包含我们访问过的所有的目的地。当我们打开应用时,应用的第一个目的地就放置在了堆栈中。每次调用navigate()方法都会将要导航到的目的地放置在堆栈的顶部。点击向上或者返回键的时候会分别调用NavController.navigateUp()NavController.popBackStack()用于移除或者弹出堆栈顶部的目的地。这两个方法均会返回一个布尔值,如果成功将目的地退出堆栈则会返回true,否则会返回false。一般情况下返回false是因为当前已经处于起始的目的地了,无法再向上导航,此时我们可能需要退出当前应用,如下代码所示:

            if (!findNavController().navigateUp()) {
                requireActivity().finish()
            }

条件导航

在很多时候,具体要导航到哪一个目的地是根据当时的条件来决定的。比如对于一个显示用户信息的页面来说,只有在用户登录的情况下才可以显示这个页面,用户如果没有登录,那么就应该导航到用户登录页面,引导用户进行登录,如果用户在登录的过程中用户取消登录,则此时就应该返回到显示用户信息之前的那个页面。

我们尝试复现上面的场景,首先创建三个页面,一个是首页,用户可以在这个页面导航到用户信息页面,在显示用户信息页面的时候会首先判断用户是否登录成功,如果登录成功则显示该页面的内容,否则导航到用户登录页面,在登录页面用户取消登录(点击返回键)后会返回到显示用户信息页面,此时继续检查用户状态,没有登录成功则导航到首页:

    <!--首页-->
    <fragment
            android:id="@+id/fragment_nav_condition_home"
            android:name="com.zyf.navstudy.nav_go.condition.ConditionHomeFragment"
            android:label="@string/nav_condition_home"
            tools:layout="@layout/fragment_condition_home"
            >

        <!--跳转到显示用户信息的页面-->
        <action
                android:id="@+id/to_user_info"
                app:destination="@id/fragment_show_user_info"
                />
    </fragment>
    <!--显示用户信息的页面-->
    <fragment
            android:id="@+id/fragment_show_user_info"
            android:name="com.zyf.navstudy.nav_go.condition.ShowUserInfoFragment"
            android:label="@string/show_user_info"
            tools:layout="@layout/fragment_show_user_info"
            >
        <!--跳转到用户登录页面-->
        <action
                android:id="@+id/to_user_login"
                app:destination="@id/fragment_user_login"
                />
        <!--导航到首页,限于用户没有登录的情况-->
        <action
                android:id="@+id/to_home"
                app:destination="@id/fragment_nav_condition_home"
                app:popUpTo="@id/fragment_nav_condition_home"
                app:popUpToInclusive="true"
                />
    </fragment>

    <!--用户登录的页面-->
    <fragment
            android:id="@+id/fragment_user_login"
            android:name="com.zyf.navstudy.nav_go.condition.UserLoginFragment"
            android:label="@string/user_login"
            tools:layout="@layout/fragment_user_login"
            >
    </fragment>

在上面的导航图中,我们定义了三个页面,分别是首页,显示用户信息页面和用户登录页面。然后我们定义了三个操作,分别是从首页导航到用户信息页,从用户信息页导航到登录页,以及从用户信息页导航到首页。

用户信息不会在任何一个Fragment中去保存,一般情况下用户信息应该是全局的,程序中的任何地方都可能会使用到用户信息,因为我们倾向于将用户信息保存在ViewModelLiveData中,这个ViewModelActivity去持有,这样这个用户信息就可以认为是全局共享的。

这里推荐大家看一下官方的文档,点击即可访问,主要是因为其中涉及到SavedStateHandle,暂时还没有完全搞明白这一块,看起来像是导航组件提供的一个保存全局状态的机制,通过LiveData保存数据从而实现数据共享。

上面的代码最终的运行效果如下:

用户取消登录的情景用户成功登录的情景