美文网首页
Jetpack之Navigation解析

Jetpack之Navigation解析

作者: 就叫汉堡吧 | 来源:发表于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