-
概述
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效果。
网友评论