Android进阶宝典 -- JetPack Navigation的高级用法(解决路由跳转新建Fragment页面问题)

Posted datian1234

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android进阶宝典 -- JetPack Navigation的高级用法(解决路由跳转新建Fragment页面问题)相关的知识,希望对你有一定的参考价值。

相信有相当一部分的伙伴,在项目开发中依然使用Activity作为页面承载体,有10个页面就会有10个Activity,这种方式当然没问题,但是如果涉及到页面间数据共享,那么使用多Activity就不是很方便了,需要Activity传递各种数据,涉及到数据的序列化与反序列化;因此产生了单Activity和多Fragment架构,所有的Fragment可以共享Activity中的数据,不需要数据传递便可操作数据,而且Fragment相较于Activity更加轻量级。

但是为什么之前使用这种架构很少呢?是因为Fragment切换以及回退栈管理比较复杂,但是Navigation出现之后,局面完全逆转了,很多人都在尝试使用单Activity和多Fragment架构,那么本节就着重介绍Navigation的使用方式。

1 Navigation的基础使用

本文主要以单Activity和多Fragment架构为例介绍

//依赖配置
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.2'

准备工作1:创建路由表,存储Fragment页面

准备工作2:创建多个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"
    android:id="@+id/navi_test"
    app:startDestination="@id/fragmentA">

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.lay.image_process.navi.FragmentA"
        android:label="FragmentA" />
    <fragment
        android:id="@+id/fragmentB"
        android:name="com.lay.image_process.navi.FragmentB"
        android:label="FragmentB" />
</navigation>

准备工作3:注册路由表

<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=".navi.NaviHolderActivity">

    <!-- defaultNavHost设置为true 回退栈将会由controller来管理-->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragmentContainerView"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" 
        app:navGraph="@navigation/navi_test" />
</androidx.constraintlayout.widget.ConstraintLayout>

如果我们需要将FragmentA和FragmentB放在一个Activity中,那么就需要使用FragmentContainerView承接,其中navGraph属性用来设置路由表,那么这张路由表中所有的Fragment都添加到了Activity中。

这样默认展示了FragmentA页面,因为在路由表中设置了起点就是fragmentA

1.1 页面跳转

前面只是完成了基础的准备工作,最关键的就是页面的跳转,那么就需要使用Navigation的能力

class NaviUtils 

    private var controller:NavController? = null

    fun inject(fragmentManager: FragmentManager,containerId:Int)
        val fragment = fragmentManager.findFragmentById(containerId) as NavHostFragment
        controller = fragment.findNavController()
    
    //动态设置路由表
    fun inject(fragmentManager: FragmentManager,containerId:Int,naviGraph: Int)
        val fragment = fragmentManager.findFragmentById(containerId) as NavHostFragment
        controller = fragment.findNavController()
        val graph = controller?.navInflater?.inflate(naviGraph)
        controller?.graph = graph!!
    
    //动态加载路由表,设置路由起点
    fun inject(fragmentManager: FragmentManager,containerId:Int,naviGraph: Int,startDestination:Int)
        val fragment = fragmentManager.findFragmentById(containerId) as NavHostFragment
        controller = fragment.findNavController()
        val graph = controller?.navInflater?.inflate(naviGraph)
        graph?.setStartDestination(startDestination)
        controller?.graph = graph!!
    

    fun jump(id:Int)
        controller?.navigate(id)
    

    companion object 

        private val controllerMap: MutableMap<Activity, NaviUtils> by lazy 
            mutableMapOf()
        

        fun register(activity: Activity): NaviUtils 
            if (!controllerMap.containsKey(activity)) 
                controllerMap[activity] = NaviUtils()
            
            return controllerMap[activity]!!
        

        fun unregister(activity: Activity) 
            if (controllerMap.containsKey(activity)) 
                controllerMap.remove(activity)
            
        
    

这里我写了一个关于Navigation路由的封装,首先NaviUtils是一个单例,提供了3个重载方法inject,如果想要拿到NavController,需要调用NavHostFragment的findNavController方法,NavHostFragment其实就是在Activity中提供容器能力的FragmentContainerView;

class NaviHolderActivity : AppCompatActivity() 

    private lateinit var binding:ActivityNaviHolderBinding
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        binding = ActivityNaviHolderBinding.inflate(layoutInflater)
        setContentView(binding.root)

        NaviUtils.register(this).inject(supportFragmentManager,R.id.fragmentContainerView)
    

    override fun onDestroy() 
        super.onDestroy()
        NaviUtils.unregister(this)
    

在拿到NavController之后,调用其navigate方法就可以任意在路由表中跳转。例如从FragmentA跳转到FragmentB。

NaviUtils.register(requireActivity()).jump(R.id.action_fragmentA_to_fragmentB)

然后我们再加一个FragmentC,从A页面可以到C页面,从B页面也可以到C页面。之后如果新增其他页面,也需要跳转到C页面,这样的话,我们需要每个页面下都写一个到C的action,这样其实也没问题,但是其实是可以给抽出来做一个全局的action

<?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"
    android:id="@+id/navi_test"
    app:startDestination="@id/fragmentA">

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.lay.image_process.navi.FragmentA"
        android:label="FragmentA" >
        <action
            android:id="@+id/action_fragmentA_to_fragmentB"
            app:destination="@id/fragmentB" />
        <action
            android:id="@+id/action_fragmentA_to_fragmentC"
            app:destination="@id/fragmentC" />
    </fragment>
    <fragment
        android:id="@+id/fragmentB"
        android:name="com.lay.image_process.navi.FragmentB"
        android:label="FragmentB" >
        <action
            android:id="@+id/action_fragmentB_to_fragmentC"
            app:destination="@id/fragmentC" />
    </fragment>
    <fragment
        android:id="@+id/fragmentC"
        android:name="com.lay.image_process.navi.FragmentC"
        android:label="FragmentC" />
</navigation>

转换后的action

<?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"
    android:id="@+id/navi_test"
    app:startDestination="@id/fragmentA">

    <action
        android:id="@+id/to_fragmentC"
        app:destination="@id/fragmentC" />

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.lay.image_process.navi.FragmentA"
        android:label="FragmentA" >
        <action
            android:id="@+id/action_fragmentA_to_fragmentB"
            app:destination="@id/fragmentB" />
    </fragment>
    <fragment
        android:id="@+id/fragmentB"
        android:name="com.lay.image_process.navi.FragmentB"
        android:label="FragmentB" >
    </fragment>
    <fragment
        android:id="@+id/fragmentC"
        android:name="com.lay.image_process.navi.FragmentC"
        android:label="FragmentC" />
</navigation>

路由表也看的比较清晰了

NaviUtils.register(requireActivity()).jump(R.id.to_fragmentC)
    putString("msg","这是从A页面传递过来的信息")

1.2 回退栈

对于Navigation回退栈,也有相关的api可以借鉴

fun backStack()
    controller?.popBackStack()


fun backStack(desId:Int,popInclusive:Boolean)
    controller?.popBackStack(desId,popInclusive)

例如,从A页面到B页面,如果返回到A页面,那么就可以调用backStack方法;像这种进入退出都可以使用popBackStack来进行退栈处理;

如果从A跳转到B,从B跳转到C,然后从C退出后,直接回到A页面,该如何处理?

因为popBackStack返回只能返回到上一级,如果我们设置一个路由,从C直接到A是不是就可以解决了呢?试一下

<action
    android:id="@+id/to_fragmentA"
    app:destination="@id/fragmentA"/>

显然不可以,虽然跳转到了A页面,但是点击app退出的时候,又回到了C页面,因此在内存中还存在B C页面的任务栈,所以想要清除B C页面任务栈,需要两个属性popUpTo和popUpToInclusive

<action
    android:id="@+id/to_fragmentA"
    app:destination="@id/fragmentA"
    app:popUpTo="@id/fragmentA"
    app:popUpToInclusive="true"/>

使用popUpTo,那么除了fragmentA之外,其他所有的页面都会出栈,那么在跳转到A页面之后,点返回按钮就直接退出app了。
当然这是静态处理,那么如果想动态配置,那么可以调用NaviUtils中的backStack第二个重载函数。

NaviUtils.register(requireActivity()).backStack(R.id.fragmentA,true)

这个方法其实与上述XML布局中描述的一致

1.3 Fragment间数据传递

在前言中,我们提到了Fragment之间数据传递,其实如果使用Navigation,Fragment之间数据传递就太简单了,我们在NaviUtils中添加一个方法

fun jump(id: Int, args: Bundle.() -> Unit) 
    val bundle = Bundle()
    bundle.args()
    controller?.navigate(id, bundle)

当页面跳转时,可以携带参数进行传递

NaviUtils.register(requireActivity()).jump(R.id.action_fragmentA_to_fragmentB)
    putString("msg","这是从A页面传递过来的信息")

那么在B页面就可以接受参数:

arguments?.let 
    val msg = it.getString("msg")
    binding.tvMsg.text = msg

2 Navigation原理分析

本小节源码为kotlin源码,其实与Java版本基本一致,如果不熟悉Kotlin的小伙伴也可以跟一下,明白原理即可

2.1 NavHostFragment

从本文一开始准备工作中知道,路由表是放在NavHostFragment当中的,所以先从NavHostFragment中的源码看起,NavHostFragment其实也是一个Fragment,并实现了NavHost接口

public interface NavHost 
    /**
     * The [navigation controller][NavController] for this navigation host.
     */
    public val navController: NavController

在NavHost接口中,有一个成员变量navController,其实就是我们用来配置导航的工具类,在NaviUtils中,我们通过id获取到NavHostFragment之后,拿到了NavController对象。

@CallSuper
public override fun onCreate(savedInstanceState: Bundle?) 
    var context = requireContext()
    navHostController = NavHostController(context)
    navHostController!!.setLifecycleOwner(this)
    while (context is ContextWrapper) 
        if (context is OnBackPressedDispatcherOwner) 
            navHostController!!.setOnBackPressedDispatcher(
                (context as OnBackPressedDispatcherOwner).onBackPressedDispatcher
            )
            // Otherwise, caller must register a dispatcher on the controller explicitly
            // by overriding onCreateNavHostController()
            break
        
        context = context.baseContext
    
    // Set the default state - this will be updated whenever
    // onPrimaryNavigationFragmentChanged() is called
    navHostController!!.enableOnBackPressed(
        isPrimaryBeforeOnCreate != null && isPrimaryBeforeOnCreate as Boolean
    )
    isPrimaryBeforeOnCreate = null
    navHostController!!.setViewModelStore(viewModelStore)
    //创建navigator
    onCreateNavHostController(navHostController!!)
    var navState: Bundle? = null
    if (savedInstanceState != null) 
        navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE)
        if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) 
            defaultNavHost = true
            parentFragmentManager.beginTransaction()
                .setPrimaryNavigationFragment(this)
                .commit()
        
        graphId = savedInstanceState.getInt(KEY_GRAPH_ID)
    
    if (navState != null) 
        // Navigation controller state overrides arguments
        navHostController!!.restoreState(navState)
    
    if (graphId != 0) 
        // Set from onInflate()
        navHostController!!.setGraph(graphId)
     else 
        // See if it was set by NavHostFragment.create()
        val args = arguments
        val graphId = args?.getInt(KEY_GRAPH_ID) ?: 0
        val startDestinationArgs = args?.getBundle(KEY_START_DESTINATION_ARGS)
        if (graphId != 0) 
            navHostController!!.setGraph(graphId, startDestinationArgs)
        
    

    // We purposefully run this last as this will trigger the onCreate() of
    // child fragments, which may be relying on having the NavController already
    // created and having its state restored by that point.
    super.onCreate(savedInstanceState)

navController是在NavHostFragment的onCreate方法中初始化的,这里是创建了一个NavHostController对象,这个类的父类就是NavController

final override val navController: NavController
    get() 
        checkNotNull(navHostController)  "NavController is not available before onCreate()" 
        return navHostController as NavHostController
    

我们看下NavController的构造方法,我们可以看到,当创建NavController时,在_navigatorProvider中添加了2个Navigator

init 
    _navigatorProvider.addNavigator(NavGraphNavigator(_navigatorProvider))
    _navigatorProvider.addNavigator(ActivityNavigator(context))

NavigatorProvider是什么,我们可以把他当做是路由表的提供者,我们之前写过的路由表navi_test就是其中之一,而且我们可以看到路由表中不仅仅支持Fragment,还支持NavGraph、Activity等

public open var navigatorProvider: NavigatorProvider
    get() = _navigatorProvider
    /**
     * @hide
     */
    set(navigatorProvider) 
        check(backQueue.isEmpty())  "NavigatorProvider must be set before setGraph call" 
        _navigatorProvider = navigatorProvider
    

先不着急看,回到之前的代码中,我们在创建了NavController之后,调用了NaviHostFragment的onCreateNavController方法,将NavController传递了进去,我们可以看到,又往NavController的navigatorProvider中添加了DialogFragmentNavigator和FragmentNavigator

protected open fun onCreateNavController(navController: NavController) 
    navController.navigatorProvider +=
        DialogFragmentNavigator(requireContext(), childFragmentManager)
    navController.navigatorProvider.addNavigator(createFragmentNavigator())

2.2 Navigator

我们看到,在NavHostFragment的onCreate方法中,创建了多种Navigator,并添加到了NavController的navigatorProvider中,那么Navigator是什么呢?我们看下源码

public abstract class Navigator<D : NavDestination> 
    /**
     * This annotation should be added to each Navigator subclass to denote the default name used
     * to register the Navigator with a [NavigatorProvider].
     *
     * @see NavigatorProvider.addNavigator
     * @see NavigatorProvider.getNavigator
     */
    @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
    @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
    public annotation class Name(val value: String)

    private var _state: NavigatorState? = null

    /**
     * Construct a new NavDestination associated with this Navigator.
     *
     * Any initialization of the destination should be done in the destination's constructor as
     * it is not guaranteed that every destination will be created through this method.
     * @return a new NavDestination
     */
    public abstract fun createDestination(): D

    /**
     * Navigate to a destination.
     *
     * Requests navigation to a given destination associated with this navigator in
     * the navigation graph. This method generally should not be called directly;
     * [NavController] will delegate to it when appropriate.
     *
     * @param entries destination(s) to navigate to
     * @param navOptions additional options for navigation
     * @param navigatorExtras extras unique to your Navigator.
     */
    @Suppress("UNCHECKED_CAST")
    public open fun navigate(
        entries: List<NavBackStackEntry>,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ) 
        entries.asSequence().map  backStackEntry ->
            val destination = backStackEntry.destination as? D ?: return@map null
            val navigatedToDestination = navigate(
                destination, backStackEntry.arguments, navOptions, navigatorExtras
            )
            when (navigatedToDestination) 
                null -> null
                destination -> backStackEntry
                else -> 
                    state.createBackStackEntry(
                        navigatedToDestination,
                        navigatedToDestination.addInDefaultArgs(backStackEntry.arguments)
                    )
                
            
        .filterNotNull().forEach  backStackEntry ->
            state.push(backStackEntry)
        
    

    
    /**
     * Navigate to a destination.
     *
     * Requests navigation to a given destination associated with this navigator in
     * the navigation graph. This method generally should not be called directly;
     * [NavController] will delegate to it when appropriate.
     *
     * @param destination destination node to navigate to
     * @param args arguments to use for navigation
     * @param navOptions additional options for navigation
     * @param navigatorExtras extras unique to your Navigator.
     * @return The NavDestination that should be added to the back stack or null if
     * no change was made to the back stack (i.e., in cases of single top operations
     * where the destination is already on top of the back stack).
     */
    // TODO Deprecate this method once all call sites are removed
    @Suppress("UNUSED_PARAMETER", "RedundantNullableReturnType")
    public open fun navigate(
        destination: D,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ): NavDestination? = destination

    /**
     * Attempt to pop this navigator's back stack, performing the appropriate navigation.
     *
     * All destinations back to [popUpTo] should be popped off the back stack.
     *
     * @param popUpTo the entry that should be popped off the [NavigatorState.backStack]
     * along with all entries above this entry.
     * @param savedState whether any Navigator specific state associated with [popUpTo] should
     * be saved to later be restored by a call to [navigate] with [NavOptions.shouldRestoreState].
     */
    @Suppress("UNUSED_PARAMETER")
    public open fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) 
        val backStack = state.backStack.value
        check(backStack.contains(popUpTo)) 
            "popBackStack was called with $popUpTo which does not exist in back stack $backStack"
        
        val iterator = backStack.listIterator(backStack.size)
        var lastPoppedEntry: NavBackStackEntry? = null
        do 
            if (!popBackStack()) 
                // Quit early if popBackStack() returned false
                break
            
            lastPoppedEntry = iterator.previous()
         while (lastPoppedEntry != popUpTo)
        if (lastPoppedEntry != null) 
            state.pop(lastPoppedEntry, savedState)
        
    

    /**
     * Attempt to pop this navigator's back stack, performing the appropriate navigation.
     *
     * Implementations should return `true` if navigation
     * was successful. Implementations should return `false` if navigation could not
     * be performed, for example if the navigator's back stack was empty.
     *
     * @return `true` if pop was successful
     */
    // TODO Deprecate this method once all call sites are removed
    public open fun popBackStack(): Boolean = true

    /**
     * Called to ask for a [Bundle] representing the Navigator's state. This will be
     * restored in [onRestoreState].
     */
    public open fun onSaveState(): Bundle? 
        return null
    

    /**
     * Restore any state previously saved in [onSaveState]. This will be called before
     * any calls to [navigate] or
     * [popBackStack].
     *
     * Calls to [createDestination] should not be dependent on any state restored here as
     * [createDestination] can be called before the state is restored.
     *
     * @param savedState The state previously saved
     */
    public open fun onRestoreState(savedState: Bundle) 

    /**
     * Interface indicating that this class should be passed to its respective
     * [Navigator] to enable Navigator specific behavior.
     */
    public interface Extras

我们这里挑几个核心方法看下,首先createDestination,是创建了一个新的NavDestination,其实我们可以把它看做是一个页面,例如下面的FragmentA,就是一个NavDestination

<fragment
    android:id="@+id/fragmentA"
    android:name="com.lay.image_process.navi.FragmentA"
    android:label="FragmentA" >
    <action
        android:id="@+id/action_fragmentA_to_fragmentB"
        app:destination="@id/fragmentB" />
</fragment>

然后核心的就是navigate方法,其实我们已经调用过这个方法了,只是通过NavController来调用的,既然在NavController中存在NavigatorProvider存储这些Navigator,我们就能想到,有可能就是调用这个方法,我们先分别看下不同的Navigator的navigate方法是什么样的:
(1)FragmentNavigator # navigate

private fun createFragmentTransaction(
    entry: NavBackStackEntry,
    navOptions: NavOptions?
): FragmentTransaction 
    //这里的 Destination就是即将跳转到的页面
    val destination = entry.destination as Destination
    val args = entry.arguments
    var className = destination.className
    if (className[0] == '.') 
        className = context.packageName + className
    
    //每次跳转,都会新建一个新的Fragment
    val frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
    frag.arguments = args
    val ft = fragmentManager.beginTransaction()
    var enterAnim = navOptions?.enterAnim ?: -1
    var exitAnim = navOptions?.exitAnim ?: -1
    var popEnterAnim = navOptions?.popEnterAnim ?: -1
    var popExitAnim = navOptions?.popExitAnim ?: -1
    if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) 
        enterAnim = if (enterAnim != -1) enterAnim else 0
        exitAnim = if (exitAnim != -1) exitAnim else 0
        popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
        popExitAnim = if (popExitAnim != -1) popExitAnim else 0
        ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
    
    ft.replace(containerId, frag)
    ft.setPrimaryNavigationFragment(frag)
    ft.setReorderingAllowed(true)
    return ft

我们可以看到,Fragment之间的跳转,是通过事务的replace方法,而且每次跳转到新的页面,都会重新创建

(2)DialogFragmentNavigator # navigate

private fun navigate(
    entry: NavBackStackEntry
) 
    val destination = entry.destination as Destination
    var className = destination.className
    if (className[0] == '.') 
        className = context.packageName + className
    
    val frag = fragmentManager.fragmentFactory.instantiate(
        context.classLoader, className
    )
    require(DialogFragment::class.java.isAssignableFrom(frag.javaClass)) 
        "Dialog destination $destination.className is not an instance of DialogFragment"
    
    val dialogFragment = frag as DialogFragment
    dialogFragment.arguments = entry.arguments
    dialogFragment.lifecycle.addObserver(observer)
    dialogFragment.show(fragmentManager, entry.id)
    state.push(entry)

DialogFragment虽然是一个Fragment,但是也是弹窗的形式存在,因此展示的时候,采用Dialog常用的show方法也是理所应当的了。

(3)ActivityNavigator # navigate

override fun navigate(
    destination: Destination,
    args: Bundle?,
    navOptions: NavOptions?,
    navigatorExtras: Navigator.Extras?
): NavDestination? 
    checkNotNull(destination.intent) 
        ("Destination $destination.id does not have an Intent set.")
    
    val intent = Intent(destination.intent)
    //......
    val destId = destination.id
    intent.putExtra(EXTRA_NAV_CURRENT, destId)
    val resources = context.resources
    if (navOptions != null) 
        val popEnterAnim = navOptions.popEnterAnim
        val popExitAnim = navOptions.popExitAnim
        if (
            popEnterAnim > 0 && resources.getResourceTypeName(popEnterAnim) == "animator" ||
            popExitAnim > 0 && resources.getResourceTypeName(popExitAnim) == "animator"
        ) 
            Log.w(
                LOG_TAG,
                "Activity destinations do not support Animator resource. Ignoring " +
                    "popEnter resource $resources.getResourceName(popEnterAnim) and " +
                    "popExit resource $resources.getResourceName(popExitAnim) when " +
                    "launching $destination"
            )
         else 
            // For use in applyPopAnimationsToPendingTransition()
            intent.putExtra(EXTRA_POP_ENTER_ANIM, popEnterAnim)
            intent.putExtra(EXTRA_POP_EXIT_ANIM, popExitAnim)
        
    
    if (navigatorExtras is Extras) 
        val activityOptions = navigatorExtras.activityOptions
        if (activityOptions != null) 
            ActivityCompat.startActivity(context, intent, activityOptions.toBundle())
         else 
            context.startActivity(intent)
        
     else 
        context.startActivity(intent)
    
    if (navOptions != null && hostActivity != null) 
        var enterAnim = navOptions.enterAnim
        var exitAnim = navOptions.exitAnim
        if (
            enterAnim > 0 && (resources.getResourceTypeName(enterAnim) == "animator") ||
            exitAnim > 0 && (resources.getResourceTypeName(exitAnim) == "animator")
        ) 
            Log.w(
                LOG_TAG,
                "Activity destinations do not support Animator resource. " +
                    "Ignoring " + "enter resource " + resources.getResourceName(enterAnim) +
                    " and exit resource " + resources.getResourceName(exitAnim) + "when " +
                    "launching " + destination
            )
         else if (enterAnim >= 0 || exitAnim >= 0) 
            enterAnim = enterAnim.coerceAtLeast(0)
            exitAnim = exitAnim.coerceAtLeast(0)
            hostActivity.overridePendingTransition(enterAnim, exitAnim)
        
    

    // You can't pop the back stack from the caller of a new Activity,
    // so we don't add this navigator to the controller's back stack
    return null

看了前面两个,对于Activity的启动方式,大概率就是通过startActivity来实现的了;

其实看到这里,我们就大概知道了,Navigation路由表中支持的节点类型了,而且每个Navigator都包装了一个页面,每个类型的Navigator都有自己展示的形式

<activity></activity>
<dialog></dialog>
<NoOp></NoOp>

2.3 navigation布局文件解析

在之前NaviUtils中,inject有3个重载方法,然后第二个重载方法支持动态加载路由表,我们看到的是,通过NavController解析路由表,然后调用setGraph方法,将解析后的路由表添加进去

 //动态设置路由表
fun inject(fragmentManager: FragmentManager,containerId:Int,naviGraph: Int)
    val fragment = fragmentManager.findFragmentById(containerId) as NavHostFragment
    controller = fragment.findNavController()
    val graph = controller?.navInflater?.inflate(naviGraph)
    controller?.graph = graph!!

在NavController中有一个成员变量,就是NavInflater对象,看名字就是路由解析器,类似于LayoutInflater

public open val navInflater: NavInflater by lazy 
    inflater ?: NavInflater(context, _navigatorProvider)

通过调用NavInflater的inflate方法,获取到一个NavGraph对象;首先我们先不看NavGraph是什么,我们看到inflate方法传入一个参数graphResId,这个就是我们的路由表文件R.navigation.xxx,它是通过XmlPullParser进行XML解析

public fun inflate(@NavigationRes graphResId: Int): NavGraph 
    val res = context.resources
    val parser = res.getXml(graphResId)
    val attrs = Xml.asAttributeSet(parser)
    return try 
        var type: Int
        while (parser.next().also  type = it  != XmlPullParser.START_TAG &&
            type != XmlPullParser.END_DOCUMENT
        )  /* Empty loop */
        
        if (type != XmlPullParser.START_TAG) 
            throw XmlPullParserException("No start tag found")
        
        val rootElement = parser.name
        val destination = inflate(res, parser, attrs, graphResId)
        require(destination is NavGraph) 
            "Root element <$rootElement> did not inflate into a NavGraph"
        
        destination
     catch (e: Exception) 
        throw RuntimeException(
            "Exception inflating $res.getResourceName(graphResId) line $parser.lineNumber",
            e
        )
     finally 
        parser.close()
    

通过2.2小节中对于Navigator的了解,我们知道NavGraph也是一种节点类型,而且一张路由表中全部的节点都是存在navigatorProvider中,然后在inflate方法中,根据路由表中节点的名字,例如fragment、dialog、action等,获取其对应的Navigator类型,然后创建其对应的Destination对象

@Throws(XmlPullParserException::class, IOException::class)
private fun inflate(
    res: Resources,
    parser: XmlResourceParser,
    attrs: AttributeSet,
    graphResId: Int
): NavDestination 
    val navigator = navigatorProvider.getNavigator<Navigator<*>>(parser.name)
    val dest = navigator.createDestination()
    dest.onInflate(context, attrs)
    val innerDepth = parser.depth + 1
    var type: Int
    var depth = 0
    while (parser.next().also  type = it  != XmlPullParser.END_DOCUMENT &&
        (parser.depth.also  depth = it  >= innerDepth || type != XmlPullParser.END_TAG)
    ) 
        if (type != XmlPullParser.START_TAG) 
            continue
        
        if (depth > innerDepth) 
            continue
        
        val name = parser.name
        if (TAG_ARGUMENT == name) 
            inflateArgumentForDestination(res, dest, attrs, graphResId)
         else if (TAG_DEEP_LINK == name) 
            inflateDeepLink(res, dest, attrs)
         else if (TAG_ACTION == name) 
            inflateAction(res, dest, attrs, parser, graphResId)
         else if (TAG_INCLUDE == name && dest is NavGraph) 
            res.obtainAttributes(attrs, androidx.navigation.R.styleable.NavInclude).use 
                val id = it.getResourceId(androidx.navigation.R.styleable.NavInclude_graph, 0)
                dest.addDestination(inflate(id))
            
         else if (dest is NavGraph) 
            dest.addDestination(inflate(res, parser, attrs, graphResId))
        
    
    return dest

然后再往下看,是一个while循环,遍历路由表中全部的节点,然后会判断每个节点中标签

private const val TAG_ARGUMENT = "argument"
private const val TAG_DEEP_LINK = "deepLink"
private const val TAG_ACTION = "action"
private const val TAG_INCLUDE = "include"

将参数赋值给当前节点并返回,最终遍历完成全部节点之后,会返回一个NavGraph对象,调用NavController的setGraph方法,传入这个路由表

public open var graph: NavGraph
    @MainThread
    get() 
        checkNotNull(_graph)  "You must call setGraph() before calling getGraph()" 
        return _graph as NavGraph
    
    @MainThread
    @CallSuper
    set(graph) 
        setGraph(graph, null)
    

我们继续看下setGraph方法,我们可以看到,如果是两张不同的路由表,那么就会直接进行替换,重新加载生成新的路由表

public open fun setGraph(graph: NavGraph, startDestinationArgs: Bundle?) 
    if (_graph != graph) 
        _graph?.let  previousGraph ->
            // Clear all saved back stacks by iterating through a copy of the saved keys,
            // thus avoiding any concurrent modification exceptions
            val savedBackStackIds = ArrayList(backStackMap.keys)
            savedBackStackIds.forEach  id ->
                clearBackStackInternal(id)
            
            // Pop everything from the old graph off the back stack
            popBackStackInternal(previousGraph.id, true)
        
        _graph = graph
        onGraphCreated(startDestinationArgs)
     else 
        for (i in 0 until graph.nodes.size()) 
            val newDestination = graph.nodes.valueAt(i)
            _graph!!.nodes.replace(i, newDestination)
            backQueue.filter  currentEntry ->
                // Necessary since CI builds against ToT, can be removed once
                // androidx.collection is updated to >= 1.3.*
                @Suppress("UNNECESSARY_SAFE_CALL", "SAFE_CALL_WILL_CHANGE_NULLABILITY")
                currentEntry.destination.id == newDestination?.id
            .forEach  entry ->
                entry.destination = newDestination
            
        
    

如果是两张一样的路由表,那么只会针对路由表中的节点进行替换,例如fragment中某个action发生变化,或者路由表的起点startDestination属性发生变化,但是需要注意是一定要重新调用setGraph方法才会生效。

所以,当我们获取到了NavController之后,就相当于已经获取到了路由表中的全部节点,就能够灵活地实现跳转、传参等操作。

2.4 小结

我这边画了一张图,总结一下Navigation是如何运作的:
(1)承载路由表的容器是NavHostFragment,它也是一个Fragment,因此在其初始化的时候,会调用其生命周期onCreate方法,在这个方法中完成NavController的初始化;
(2)在NavController中,存在一个Map集合,用于存储路由表中的节点,而且支持4种类型,分别为fragment、dialog、activity、navgraph,在NavController的构造方法中初始化完成;
(3)NavController初始化完成之后,将会解析默认的路由表,路由表解析是通过XmlPullParser来完成的,会通过while循环遍历获取节点,然后通过NavigatorProvider获取当前节点的类型,创建对应的页面,最终解析完成后,是将路由表转换为NavGraph对象

还有就是对于Navigator与NavDestination的关系就是:具体的NavDestination是通过特定的Navigator创建的(调用createDestination方法),里面存储了当前节点的信息

但是页面之间跳转的逻辑,都是由Navigator来实现的,所以我们在调用NavController来进行页面跳转的时候,其实真正的执行类就是Navigator,伙伴们都明白了吧。

3 Navigation优化

我们从之前源码中可以看到,当每次调用navigate方法的时候,都会新建一个Fragment的,而且无论当前Fragment是否已经存在了,都会新建,这样的话其实是比较浪费资源的,我们可以先验证一下。

2022-09-25 14:27:19.885 23844-23844/com.lay.image_process D/TAG: FragmentA onCreateView
2022-09-25 14:28:36.009 23844-23844/com.lay.image_process D/TAG: FragmentB onCreateView
2022-09-25 14:28:37.786 23844-23844/com.lay.image_process D/TAG: FragmentC onCreateView
2022-09-25 14:28:41.311 23844-23844/com.lay.image_process D/TAG: FragmentA onCreateView

从A切换到B,从B到C,再从C到A,每次跳转都是创建一个新的Fragment,那么罪魁祸首就是FragmentNavigator的navigate方法,跳转的方式是通过replace的方式,将新建的Fragment替换

val frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
frag.arguments = args
val ft = fragmentManager.beginTransaction()
var enterAnim = navOptions?.enterAnim ?: -1
var exitAnim = navOptions?.exitAnim ?: -1
var popEnterAnim = navOptions?.popEnterAnim ?: -1
var popExitAnim = navOptions?.popExitAnim ?: -1
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) 
    enterAnim = if (enterAnim != -1) enterAnim else 0
    exitAnim = if (exitAnim != -1) exitAnim else 0
    popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
    popExitAnim = if (popExitAnim != -1) popExitAnim else 0
    ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)

ft.replace(containerId, frag)

那么既然系统的方案是这么做的,那么我们也可以通过自己创建Navigator改变这种交互方式。

3.1 自定义Navigator

因为Fragment的创建,是通过FragmentNavigator来完成,因此如果想要修改Fragment的启动方式,通过继承FragmentNavigator,重写navigate方法即可。

通过前面对源码的解读,我们知道这个问题处理的核心就是解决createFragmentTransaction方法中Fragment的展示方式,处理方式如下:

val ft = fragmentManager.beginTransaction()
//首先获取当前展示的Fragment
val primaryNavigationFragment = fragmentManager.primaryNavigationFragment
//将当前展示的Fragment隐藏
ft.hide(primaryNavigationFragment!!)
//获取即将展示的Fragment
val tag = destination.id.toString()
var frag = fragmentManager.findFragmentByTag(tag)
//如果在fragmentManager中能获取到这个Fragment,说明已经创建过这个Fragment
if (frag != null) 
    ft.show(frag)
 else 
    //如果没有,就需要创建新的Fragment
    frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
    //将其放入fragmentManager中
    frag.arguments = args
    ft.add(frag, tag)

对于Fragment的显示和隐藏,通过FragmentTransaction来实现,对于没有创建过的Fragment是采用Navigation中原有的实现逻辑,创建新的Fragment,并添加到FragmentManager中,方便下次获取。

class MyFragmentNavigator(
    private val context: Context,
    private val fragmentManager: FragmentManager,
    private val containerId: Int
) : FragmentNavigator(context, fragmentManager, containerId) 

    private var savedIds: MutableSet<String>? = null

    override fun navigate(
        entries: List<NavBackStackEntry>,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?
    ) 
        if (fragmentManager.isStateSaved) 
            Log.i(
                TAG, "Ignoring navigate() call: FragmentManager has already saved its state"
            )
            return
        
        for (entry in entries) 
            navigate(entry, navOptions, navigatorExtras)
        
    

    private fun navigate(
        entry: NavBackStackEntry,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?
    ) 

        //获取saveIds
        val savedIdsField = FragmentNavigator.javaClass.getDeclaredField("savedIds")
        savedIdsField.isAccessible = true
        //获取这个属性的值
        savedIds = savedIdsField.get(this) as MutableSet<String>

        val initialNavigation = state.backStack.value.isEmpty()
        val restoreState = (
                navOptions != null && !initialNavigation &&
                        navOptions.shouldRestoreState() &&
                        savedIds!!.remove(entry.id)
                )
        if (restoreState) 
            // Restore back stack does all the work to restore the entry
            fragmentManager.restoreBackStack(entry.id)
            state.push(entry)
            return
        
        val ft = createFragmentTransaction(entry, navOptions)

        if (!initialNavigation) 
            ft.addToBackStack(entry.id)
        

        if (navigatorExtras is Extras) 
            for ((key, value) in navigatorExtras.sharedElements) 
                ft.addSharedElement(key, value)
            
        
        ft.commit()
        // The commit succeeded, update our view of the world
        state.push(entry)
    

    private fun createFragmentTransaction(
        entry: NavBackStackEntry,
        navOptions: NavOptions?
    ): FragmentTransaction 
        val destination = entry.destination as Destination
        val args = entry.arguments
        var className = destination.className
        if (className[0] == '.') 
            className = context.packageName + className
        

        val ft = fragmentManager.beginTransaction()
        //首先获取当前展示的Fragment
        val primaryNavigationFragment = fragmentManager.primaryNavigationFragment
        //将当前展示的Fragment隐藏
        ft.hide(primaryNavigationFragment!!)
        //获取即将展示的Fragment
        val tag = destination.id.toString()
        var frag = fragmentManager.findFragmentByTag(tag)
        //如果在fragmentManager中能获取到这个Fragment,说明已经创建过这个Fragment
        if (frag != null) 
            ft.show(frag)
         else 
            //如果没有,就需要创建新的Fragment
            frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
            //将其放入fragmentManager中
            frag.arguments = args

            //注意这里需要加到containerId里,不然不会显示Fragment的UI
            ft.add(containerId,frag, tag)

        

        var enterAnim = navOptions?.enterAnim ?: -1
        var exitAnim = navOptions?.exitAnim ?: -1
        var popEnterAnim = navOptions?.popEnterAnim ?: -1
        var popExitAnim = navOptions?.popExitAnim ?: -1
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) 
            enterAnim = if (enterAnim != -1) enterAnim else 0
            exitAnim = if (exitAnim != -1) exitAnim else 0
            popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
            popExitAnim = if (popExitAnim != -1) popExitAnim else 0
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
        
        ft.setPrimaryNavigationFragment(frag)
        ft.setReorderingAllowed(true)
        return ft
    

    companion object 
        private const val TAG = "MyFragmentNavigator"
    

3.2 自定义Navigator注入

既然我们写了一个自己的MyFragmentNavigator,那么怎么能放在navigation这个框架里使用呢?我们先看下原始的FragmentNavigator,有一个Navigator.Name的注解,细心的伙伴可能就发现了,这个就是我们在路由表中加入的标签。

@Navigator.Name("fragment")
public open class FragmentNavigator(
    private val context: Context,
    private val fragmentManager: FragmentManager,
    private val containerId: Int
) : Navigator<Destination>() 

那么我们也可以自己定义一个标签 - my_fragment,这样当解析到这类标签的时候,就会使用我们自定义的CustomFragmentNavigator去创建页面,页面之间的跳转也会使用我们自定义的navigate,从而避免重复创建的问题

@Navigator.Name("my_fragment")
class CustomFragmentNavigator(
    private val context: Context,
    private val fragmentManager: FragmentManager,
    private val containerId: Int
) : FragmentNavigator(context, fragmentManager, containerId) 

通过前面源码的阅读,我们知道FragmentNavigator是在NavHostFragment的onCreate方法中,调用onCreateNavHostController中创建的,因此我们可以继承NavHostFragment,重写这个方法,将我们自定义的CustomFragmentNavigator添加到NavController中

class MyNavHostFragment : NavHostFragment() 
    
    override fun onCreateNavController(navController: NavController) 
        navController.navigatorProvider += CustomFragmentNavigator(
            requireContext(),
            fragmentManager = childFragmentManager,
            id
        )
        super.onCreateNavController(navController)
    

这样,我们的navigation路由表需要做一些改造,将fragment标签换成我们自定义的my_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"
    android:id="@+id/navi_test"
    app:startDestination="@id/fragmentA">

    <action
        android:id="@+id/to_fragmentC"
        app:destination="@id/fragmentC" />

    <action
        android:id="@+id/to_fragmentA"
        app:destination="@id/fragmentA"/>

    <my_fragment
        android:id="@+id/fragmentA"
        android:name="com.lay.image_process.navi.FragmentA"
        android:label="FragmentA" >
        <action
            android:id="@+id/action_fragmentA_to_fragmentB"
            app:destination="@id/fragmentB" />
    </my_fragment>
    <my_fragment
        android:id="@+id/fragmentB"
        android:name="com.lay.image_process.navi.FragmentB"
        android:label="FragmentB" >
    </my_fragment>
    <my_fragment
        android:id="@+id/fragmentC"
        android:name="com.lay.image_process.navi.FragmentC"
        android:label="FragmentC" />

</navigation>

还需要注意一点的就是,FragmentContainerView中的NavHostFragment要替换成我们自定义的NavHostFragment。

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/fragmentContainerView"
    android:name="com.lay.image_process.utils.MyNavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/navi_test" />

我们可以自己验证一下,当使用Google原生的Navigation,跳转回退都会新建新的Fragment;当使用我们自己的Navigator后,就不再有这种情况。

作者:想要成为专家的Lay
链接:https://juejin.cn/post/7147265281973288973
更多Android学习资料可点击下方卡片了解~

以上是关于Android进阶宝典 -- JetPack Navigation的高级用法(解决路由跳转新建Fragment页面问题)的主要内容,如果未能解决你的问题,请参考以下文章

Android Jetpack架构组件(入门教程及进阶实战)独家首发

Android安卓进阶技术之——Jetpack Compose

Android kotlin 系列讲解(进阶篇)Jetpack系列之LiveData

Android安卓进阶技巧之Kotlin结合Jetpack构建MVVM

Kotlin基础从入门到进阶系列讲解(进阶篇)Jetpack,(更新中)

Android进阶宝典 -- CameraX与Camera2的使用比对