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

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

时间:2023-06-19 12:03:05浏览次数:47  
标签:进阶 val -- destination Fragment 跳转 Navigator fragmentManager 路由表


相信有相当一部分的伙伴,在项目开发中依然使用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页面

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

准备工作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中。

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

这样默认展示了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>

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

转换后的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>

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

路由表也看的比较清晰了

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>

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

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 小结

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

我这边画了一张图,总结一下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方法),里面存储了当前节点的信息

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

但是页面之间跳转的逻辑,都是由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

标签:进阶,val,--,destination,Fragment,跳转,Navigator,fragmentManager,路由表
From: https://blog.51cto.com/u_16163442/6512315

相关文章

  • 曾经运维生涯中的几个“最”
    建荣写过一篇《维护之夜,说点故事和经验》,讲了一些维护工作中碰到的事情,虽然我的本职工作不是DBA,但是已经从事了十多年的应用运维工作,应用同样需要各种维护,包括上线投产、应急处置、迁移、演练等,只是维护的对象不仅仅是数据库,而是应用为核心展开的,在这个过程中,碰到了很多让我印象深......
  • Android开发卷吗?卷;怎么破?从Framework提升自己——字节总监
    前言现在客户端卷的风起云涌,很多安卓开发者都是抱怨连天。内卷之下,相比本来就堪忧的发量,前途未卜的迷茫带来的精神折磨更是雪上加霜。其实平心而论,很多开发者对现有知识都掌握的不够扎实,例如Framework。对于大部分的开发者来说,AndroidFrameWork都无法做到精通,更别提触类旁通了。想......
  • 2022最新 Android 中高级面试题汇总(含答案解析)
    准备面试少不了Android面试题复习,面试题也需要有方法,高效的进行复习,所以给大家分享一份《2022最新Android中高级面试题汇总》,基本涵盖了Android中高级工程师面试必备知识点,希望可以帮助一些想要跳槽涨薪的朋友更好、更高效的复习,实现跳槽涨薪。Android中高级面试题主要包含知识......
  • Windows服务器定时重启设置教程 103.216.155.x
    Windows系统的任务计划程序,可以添加计划任务,设置任务开始时间及执行的间隔,实现应用的自动执行。例如:实现定时重启、关机等常见的功能。如何使用参考以下步骤1、新建一个文本文件,将文件后缀改为bat,然后添加如下代码shutdown-r-f-t 0该命令的作用是立即强制重启机器。在文件中单......
  • 欧洲杯第一周的比赛闲聊
    欧洲杯开赛的第一周,精彩纷呈的同时,有点五味杂陈,有竞技层面的,有人性的考验,好戏才刚刚开始。对球员和球迷来说这届的欧洲杯来之不易,来到现场的球迷,更是不容易,从某种层面,参赛的球队、球员、球迷都是赢家,虽然某些球场,按照防疫要求,不能装满球迷,但无论是现场,还是电视机前,所有的球迷,都在为......
  • 欧洲杯第三周的比赛闲聊
    本周小组赛决出了胜负,几家欢喜几家愁,由于赛制的特殊,小组第三都是有机会出线的,因此竞争起来,其实不是特激烈,但是有几支球队,还是靠着净胜球,惊险晋级。意大利的男模队,延续了良好状态,即使铁定出线,还是极具体育道德地1:0战胜了欧洲红龙,这届大赛的意大利吊足了球迷的胃口,难道曾经的世界冠......
  • 陪玩圈子系统APP小程序H5,内容了解。
    服务器配置为2G4核带宽2M,此配置为腾讯新客户低价配置。价格便宜,多数用户起步阶段均购买此配置。点击咨询完整陪玩系统ab-n100-c10100并发访问10次我想用小程序访问可以不?答:UNIAPP可以打包成小程序,但小程序个人就不要考虑了。无法审核和申请类目。3、我想用公众号就是H5来......
  • 腾讯薪选T9级Android进阶学习路线(内含全套学习笔记+视频资料)
    随着android系统的迭代更新和开源api的强大,相信大部分开发者技术的瓶颈很难突破。想要突破瓶颈,我们就需要搭建完备的知识体系,让学到的知识体系化。这里,我整理了Android开发中的一些知识点,包括架构师筑基必备技能、源码解析、性能优化、Kotlin、高级UI、NDK模块、Flutter及微信小程......
  • 特别的高考
    今天又是一年一度的高考了,因为新冠疫情,让这次高考,变得很特别。此时此刻,你能相信的,只是你自己。高考只是人生中的一站,但确实能让你在尽可能公平的情况下,赢得自己未来的一站,因此到了现在,唯有战斗,拼了。加油,高考。......
  • 腾讯T9级Android音视频开发学习路线(初-中-高)
    腾讯T10级Android音视频开发学习路线(初-中-高)随着5G时代的开启,音视频的运用不再单一局限于泛娱乐平台,而是向多个领域延伸和完善,比如远程教学、在线教学、在线会议、远程互动、安防监控等,传统的图文媒体也开始向以音视频为主的新媒体转变,这些变化极大地改变了我们的生活。如今,行业对......