Navigation 简明介绍 && 避坑指南

2,255 阅读4分钟

介绍

相信绝大多数的Android开发者对fragment都有或多或少的使用经历。相对于activityfragment具有更优异的性能表现。相对于Viewfragment更易管理。当然还有fragment诞生的原因-平板适配。fragment虽然有这些优点,但是在使用的时候也是有比较棘手的地方:

  • 复杂的生命周期
  • 嵌套情况下的各种问题
  • 回退栈的维护 面对以上问题,不少前辈针对fragment的管理造了一个个优秀的轮子,比如 Fragmentation,这些优秀的轮子现在也已经大多不维护了。而我们在使用Fragmentation时,虽然不需要自己过于去关心fragment的嵌套,生命周期,以及回退栈等,当然大部分情况下他也是能正常工作的。但是相应的,遇到的问题也是层出不穷。如果有官方的 fragment 管理类的轮子那该多好。

其实Google早在Jetpack推出时就已经附带了一个fragment管理组件Navigation:

导航是指支持用户导航、进入和退出应用中不同内容片段的交互。Android Jetpack 的导航组件可帮助您实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。导航组件还通过遵循一套既定原则来确保一致且可预测的用户体验。

组成

Navigation由以下三部分组成:

  • 导航图: 存放在res/navigation目录下,用来描述导航相关信息的 XML 资源
  • NavHost: 显示导航图中目标的空白容器,其实就是用来放置 fragment的父容器
  • NavController: 真正的用于导航动作实现的控制者

集成

build.gradle中:

 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-rc01"

使用

之后我们就可以在相应的module创建navigation所属的resource目录

image.png 然后新建一个navigationresource文件

<?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/main_navigation"
    app:startDestination="...">
</navigation>

这个时候还没有向里面添加 fragment,我们向项目中添加三个 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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.example.myapplication.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.example.myapplication.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.example.myapplication.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

此时就可以在Android Studio中可视化编辑了,我们可以随意拖动来实现不同 fragment间的跳转路径,这时切回xml可以看到生成了名叫action的子节点,子结点中的destination则定义了要跳转的目标fragment

<fragment
    android:id="@+id/navigation_home"
    android:name="com.example.myapplication.ui.home.HomeFragment"
    android:label="@string/title_home"
    tools:layout="@layout/fragment_home" >
    <action
        android:id="@+id/action_navigation_home_to_navigation_dashboard3"
        app:destination="@id/navigation_dashboard" />
</fragment>

image.png 然后就是 Activity 的配置,在相应的布局文件中添加一个 fragment或者FragmentContainerView并将其name属性设置为:

"androidx.navigation.fragment.NavHostFragment"

NavHostFragment其实是作为Navigationfragment的显示载体来出现的。在来看,还有个属性defaultNavHostapp:defaultNavHost="true" 表示让当前的导航容器处理系统back操作,所有的入栈出栈行为都会在当前的容器之上。

navGraph属性则指向编写的导航图

<?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"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <fragment
        android:id="@+id/nav_host_fragment_activity_main"
        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/main_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

举个栗子,在HomeFragment跳转至DashboardFragment:

class HomeFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val textView: TextView = binding.textHome
       
        textView.setOnClickListener {
            Navigation.findNavController(it).navigate(R.id.action_navigation_home_to_navigation_dashboard3)
        }
        return root
    }
}

嵌套

可以封装其他的导航图,具体的实现可以是navigation节点下接着描述navigation节点,也可以是include引用其他导航图的资源

deepLink

深层链接,在需要配置的导航图中fragment节点下

<deepLink app:uri="www.deeplink.com/{params}" />

为相应的Activity设置 <nav-graph/> 标签

<activity android:name=".DeepLinkActivity">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>

        <!-- 为Activity设置<nav-graph/>标签 -->
        <nav-graph android:value="@navigation/graph_deep_link_activity" />

    </activity>

deeplink 变相提供了一种路由功能,适用于 app 内的各种业务跳转以及推送跳转等

问题

生命周期

使用Navigation进行fragment的切换时,观察fragment的生命周期,发现每次都会进行view的销毁与重建,当然这不在我们的预期之内,我们的预期是,已经初始化过的fragment不需要在初始化,回退回去的时候,他不用重建,还是原来的样子就好。但是为什么fragment会重新初始化界面呢,这里有详细的解答,一言概之就是Navigation是通过replace来实现fragment的切换的,因此每次切换都会重建。已经有大佬实现了一个通过hideshow来实现fragment切换的Navigation

当然,Google设计Navigation的意图,也可能是希望开发者充分利用ViewModelLiveData,配合Navigation来达到最好的效果。因为ViewModel是不受Fragment重建影响的,在 replace 之后,前一个fragment并不会走onDestroy而是会走onDestroyView,而onDestroyView并不会造成ViewModel的销毁,当再次回到这个fragment页面重新回调onViewCreated的时候,页面对ViewModel中的LiveData重新进行了obderveLiveData之前保存过这个页面的数据,因此页面上也会重新填充。

2.4.0 版本后已修复这个特性