美文网首页
Jetpack之Navigation解析

Jetpack之Navigation解析

作者: Horps | 来源:发表于2023-03-08 18:54 被阅读0次
  • 概述

    Navigation组件本意是用来管理Fragment为主导的页面导航的,如果你的Activity使用很多Fragment来进行界面切换,那你可能遇到的嵌套情况会很复杂,比如Activity中嵌套了Fragment,Fragment又会打开很多层子Fragment,如果通过FragmentManager去手动管理则需要很多额外的代码处理,Navigation组件就是为了简化这个操作的。

    它在一个集中位置配置所有导航相关信息的 XML 资源,然后通过对应的id来进行导航动作,这使得你可以通过studio或者xml文件一览界面导航关系。

    但同时,它存在着一些弊端,在处理一些特别场景时灵活度不够,因此,我们不能被它的用法限制,不要企图通过它来完成所有的最佳实践,具体场景要使用多种方式组合的形式来创造最优解。

    下面我们通过源码来一窥它的原理,也会在这个过程中明白它的不足。

  • 使用

    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    
        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/nav_main_fragment"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/bottom_nav"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_main" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    在setContentView之后(因为要等到xml加载完成),代码中:

    //找到加载的NavHostFragment
    val curNavHostFragment = supportFragmentManager.findFragmentById(R.id.nav_main_fragment) as NavHostFragment
    //获取NavHostFragment的NavController
    val navController = curNavHostFragment.navController
    //通过NavController进行导航
    navController.navigate(R.id.action_switchToTab1, bundle)
    
  • xml加载

    在FragmentContainerView的构造方法中:

    internal constructor(
        context: Context,
        attrs: AttributeSet,
        fm: FragmentManager
    ) : super(context, attrs) {
        var name = attrs.classAttribute
        var tag: String? = null
        context.withStyledAttributes(attrs, R.styleable.FragmentContainerView) {
            if (name == null) {
                name = getString(R.styleable.FragmentContainerView_android_name)
            }
            tag = getString(R.styleable.FragmentContainerView_android_tag)
        }
          //因为下面add时是按照containerId去作为fragment的id添加的,所以这里按照当前container的id来查找
        val id = id
        val existingFragment: Fragment? = fm.findFragmentById(id)
        if (name != null && existingFragment == null) {
              //当前FragmentContainerView一定得有id
            if (id == View.NO_ID) {
                val tagMessage = if (tag != null) " with tag $tag" else ""
                throw IllegalStateException(
                    "FragmentContainerView must have an android:id to add Fragment $name$tagMessage"
                )
            }
              //反射创建NavHostFragment实例
            val containerFragment: Fragment =
                fm.fragmentFactory.instantiate(context.classLoader, name)
              //onInflate方法中解析FragmentContainerView的其他xml配置属性
            containerFragment.onInflate(context, attrs, null)
              //FragmentContainerView继承自FrameLayout,这里把NavHostFragment放到FragmentContainerView中
            fm.beginTransaction()
                .setReorderingAllowed(true)
                .add(this, containerFragment, tag)
                .commitNowAllowingStateLoss()
        }
          ...
    }
    

    看一下NavHostFragment的onInflate方法:

    @CallSuper
    public override fun onInflate(
        context: Context,
        attrs: AttributeSet,
        savedInstanceState: Bundle?
    ) {
        super.onInflate(context, attrs, savedInstanceState)
        context.obtainStyledAttributes(
            attrs,
            androidx.navigation.R.styleable.NavHost
        ).use { navHost ->
            val graphId = navHost.getResourceId(
                androidx.navigation.R.styleable.NavHost_navGraph, 0
            )
            if (graphId != 0) {
                this.graphId = graphId
            }
        }
        context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment).use { array ->
            val defaultHost = array.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false)
            if (defaultHost) {
                defaultNavHost = true
            }
        }
    }
    

    可以看到,这里会取到navGraph和defaultNavHost属性的值。

    关于defaultNavHost的意义,我们看一下Activity的onBackPressed方法:

    public void onBackPressed() {
        if (mActionBar != null && mActionBar.collapseActionView()) {
            return;
        }
        FragmentManager fragmentManager = mFragments.getFragmentManager();
        if (!fragmentManager.isStateSaved() && fragmentManager.popBackStackImmediate()) {
            return;
        }
        navigateBack();
    }
    

    popBackStackImmediate方法最终:

    private boolean popBackStackImmediate(@Nullable String name, int id, int flags) {
        execPendingActions(false);
        ensureExecReady(true);
    
        if (mPrimaryNav != null // We have a primary nav fragment
                && id < 0 // No valid id (since they're local)
                && name == null) { // no name to pop to (since they're local)
            final FragmentManager childManager = mPrimaryNav.getChildFragmentManager();
            if (childManager.popBackStackImmediate()) {
                // We did something, just not to this specific FragmentManager. Return true.
                return true;
            }
        }
          ...
        return executePop;
    }
    

    这里的意思就是如果该FragmentManager还管理着子Fragment的话会先响应子childFragmentManager的返回栈操作,否则会直接将该Fragment给出栈,这也是Navigation组件的核心原理之一,通过Fragment管理子Fragment的方式进行导航。

    在NavHostFragment的onAttach方法中:

    public override fun onAttach(context: Context) {
        super.onAttach(context)
        if (defaultNavHost) {
            parentFragmentManager.beginTransaction()
                .setPrimaryNavigationFragment(this)
                .commit()
        }
    }
    

    会根据defaultNavHost把当前的NavHostFragment作为子Fragment的管理者。

  • onCreate方法

    在NavHostFragment的onCreate方法中:

    @CallSuper
    public override fun onCreate(savedInstanceState: Bundle?) {
        var context = requireContext()
        navHostController = NavHostController(context)
        navHostController!!.setLifecycleOwner(this)
        ...
        navHostController!!.setViewModelStore(viewModelStore)
        onCreateNavHostController(navHostController!!)
        ...
        if (graphId != 0) {
            navHostController!!.setGraph(graphId)
        } else {
            // 代码方式创建的
            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)
            }
        }
        super.onCreate(savedInstanceState)
    }
    

    setGraph方法:

    public open fun setGraph(@NavigationRes graphResId: Int) {
        setGraph(navInflater.inflate(graphResId), null)
    }
    

    NavInflater.inflate系列方法缩减如下:

    private fun inflate(
        res: Resources,
        parser: XmlResourceParser,
        attrs: AttributeSet,
        graphResId: Int
    ): NavDestination {
          //获取解析对应标签的Navigator
        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)
        ) {
            ...
            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) {
                  //添加到NavGraph的nodes中
                dest.addDestination(inflate(res, parser, attrs, graphResId))
            }
        }
        return dest
    }
    

    navigatorProvider的值从哪里设置的呢?

    在NavController中:

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

    在NavHostFragment的onCreateNavController方法中(还记得这个是在onCreate中调用的嘛):

    protected open fun onCreateNavController(navController: NavController) {
          //dialog标签的
        navController.navigatorProvider +=
            DialogFragmentNavigator(requireContext(), childFragmentManager)
          //fragment标签的
        navController.navigatorProvider.addNavigator(createFragmentNavigator())
    }
    

    addNavigator方法中会调用getNameForNavigator(navigator.javaClass)方法生成_navigatorProvider的key:

    internal fun getNameForNavigator(navigatorClass: Class<out Navigator<*>>): String {
        var name = annotationNames[navigatorClass]
        if (name == null) {
            val annotation = navigatorClass.getAnnotation(
                Navigator.Name::class.java
            )
            name = annotation?.value
            require(validateName(name)) {
                "No @Navigator.Name annotation found for ${navigatorClass.simpleName}"
            }
            annotationNames[navigatorClass] = name
        }
        return name!!
    }
    

    这里会解析对应Navigator类上的注解,所以最终_navigatorProvider中的键值对应关系是诸如“navigator”:NavGraphNavigator()、“fragment”:FragmentNavigator()这种。

    继承自Navigator的具体类的注解如下这样:

    @Navigator.Name("navigation")
    public open class NavGraphNavigator()
    
    @Navigator.Name("activity")
    public open class ActivityNavigator()
    

    然后调用他们的createDestination方法获取一个Destnation对象,调用它的onInflate方法,以FragmentNavigator为例:

    public override fun onInflate(context: Context, attrs: AttributeSet) {
        super.onInflate(context, attrs)
        context.resources.obtainAttributes(attrs, R.styleable.FragmentNavigator).use { array ->
            val className = array.getString(R.styleable.FragmentNavigator_android_name)
            if (className != null) setClassName(className)
        }
    }
    

    super是NavDestination,它的onInflate方法如下:

    public open fun onInflate(context: Context, attrs: AttributeSet) {
        context.resources.obtainAttributes(attrs, R.styleable.Navigator).use { array ->
            route = array.getString(R.styleable.Navigator_route)
    
            if (array.hasValue(R.styleable.Navigator_android_id)) {
                id = array.getResourceId(R.styleable.Navigator_android_id, 0)
                idName = getDisplayName(context, id)
            }
            label = array.getText(R.styleable.Navigator_android_label)
        }
    }
    

    可以看到,这里把fragment标签指定的id、name、route等属性值获取到。

    setGraph方法中会调用onGraphCreated方法,其内部会调用navigate(_graph!!, startDestinationArgs, null, null),最终会调用到NavGraphNavigator的navigate方法:

    private fun navigate(
        entry: NavBackStackEntry,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ) {
        val destination = entry.destination as NavGraph
        val args = entry.arguments
        val startId = destination.startDestinationId
        val startRoute = destination.startDestinationRoute
        check(startId != 0 || startRoute != null) {
            ("no start destination defined via app:startDestination for ${destination.displayName}")
        }
        val startDestination = if (startRoute != null) {
            destination.findNode(startRoute, false)
        } else {
            destination.findNode(startId, false)
        }
        requireNotNull(startDestination) {
            val dest = destination.startDestDisplayName
            throw IllegalArgumentException(
                "navigation destination $dest is not a direct child of this NavGraph"
            )
        }
        val navigator = navigatorProvider.getNavigator<Navigator<NavDestination>>(
            startDestination.navigatorName
        )
        val startDestinationEntry = state.createBackStackEntry(
            startDestination,
            startDestination.addInDefaultArgs(args)
        )
        navigator.navigate(listOf(startDestinationEntry), navOptions, navigatorExtras)
    }
    

    findNode会从之前添加的nodes中找到对应的Destination,,然后调用对应Navigator的navigate方法。

  • navigate方法

    不管是通过NavController.navigate手动导航还是onCreate流程中直接调用navigator.navigate的起始路由导航,最终都是通过对应Navigator实现类的navigate方法完成的,比如FragmentNavigator来说,它的navigate方法如下:

    private fun navigate(
        entry: NavBackStackEntry,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?
    ) {
        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 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
    }
    

    可以看到,每次调用navigate方法,Fragment都会重新创建,而且这些方法都不允许重写,因此无法修改这个默认实现,这是Navigation的一个弊端,灵活性不足,如果现在你有一个底部导航栏,你肯定希望导航的几个Fragment实例一直存在,然后通过show和hide来控制切换,但是Navigation每次都会重建Fragment实例

    一种折衷的办法就是使用Activity的ViewModel来保持数据不变,但是每次导航还是需要使用旧数据来再次填充新页面,在首页的场景下,很明显这种频繁创建新实例的方式是不合适的。

  • 总结

    结合源码分析,我觉得Navigation不适合不需要重复创建Fragment的场景,这种场景下一般Fragment数量不多,适合自己控制。Navigation更适合处理把Fragment当作Activity来使用的场景,即叠加效果场景时,Navigation还适用于处理Activity导航(ActivityNavigator)和Dialog导航(DialogFragmentNavigator),这些都需要重复创建实例,也适合返回的堆栈处理,通过launchSingleTop、popUpTo和popUpToInclusive也能实现仿照Activity的launchMode效果。

相关文章

网友评论

      本文标题:Jetpack之Navigation解析

      本文链接:https://www.haomeiwen.com/subject/vppoldtx.html