发布时间:2025-12-09 11:51:57 浏览次数:1
大家好,我叫小琪;
本人16年毕业于中南林业科技大学软件工程专业,毕业后在教育行业做安卓开发,后来于19年10月加入37手游安卓团队;
目前主要负责国内发行安卓相关开发,同时兼顾内部几款App开发。
上篇对Navigation的一些概念进行了介绍,并在前言中提到了app中常用的一个场景,就是app的首页,一般都会由一个activity+多个子tab组成,这种场景有很多种实现方式,比如可以使用RadioGroup、FrgmentTabHost、TabLayout或者自定义view等方式,但这些都离不开经典的FragmentManager来管理fragment之间的切换。
现在,我们有了新的实现方式,Navigation+BottomNavigationView,废话不多说,先看最终要实现的效果
先确保引入了navigation相关依赖
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'很简单,包含三个页面,首页、发现、我的,点击底部可以切换页面,有了上一篇的基础,先新建一个nav_graph的导航资源文件,包含三个framgent子节点
<?xml version="1.0" encoding="utf-8"?><navigation xmlns:andro xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android: app:startDestination="@id/FragmentHome"> <fragment android: android:name="com.example.testnavigation.FragmentHome" android:label="fragment_home" tools:layout="@layout/fragment_home"> </fragment> <fragment android: android:name="com.example.testnavigation.FragmentDicover" android:label="fragment_discover" tools:layout="@layout/fragment_discover"> </fragment> <fragment android: android:name="com.example.testnavigation.FragmentMine" android:label="fragment_mine" tools:layout="@layout/fragment_mine"> </fragment></navigation>然后在activity的布局中(这里为MainActivity的activity_main)中添加BottomNavigationView控件,
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:andro 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=".MainActivity"> <fragment android: android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:navGraph="@navigation/nav_graph" /> <com.google.android.material.bottomnavigation.BottomNavigationView android: android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:menu="@menu/bottom_nav_menu" /></androidx.constraintlayout.widget.ConstraintLayout>其中fragment节点在上面已经介绍过了,这篇不再讲解,BottomNavigationView是谷歌的一个实现底部导航的组件, app:menu属性为底部导航栏指定元素,新建一个bottom_nav_menu的menu资源文件
<?xml version="1.0" encoding="utf-8"?><menu xmlns:andro> <item android: android:icon="@mipmap/icon_tab_home" android:title="首页" /> <item android: android:icon="@mipmap/icon_tab_find" android:title="发现" /> <item android: android:icon="@mipmap/icon_tab_mine" android:title="我的" /></menu>注意:这里item标签的id和上面nav_graph中fragment标签的id一致
资源准备好后,在MainActivity中
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //fragment的容器视图,navHost的默认实现——NavHostFragment val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment //管理应用导航的对象 val navController = navHostFragment.navController //fragment与BottomNavigationView的交互交给NavigationUI bottom_nav_view.setupWithNavController(navController) }}通过NavigationUI库,将BottomNavigationView和navigation关联,就能实现上面的效果图了,是不是so easy!
是不是很疑惑,这是怎么做到的?,此时我们进到源码看看,进入setupWithNavController方法
fun BottomNavigationView.setupWithNavController(navController: NavController) { NavigationUI.setupWithNavController(this, navController)}再进入
public static void setupWithNavController( @NonNull final BottomNavigationView bottomNavigationView, @NonNull final NavController navController) { bottomNavigationView.setOnNavigationItemSelectedListener( new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { return onNavDestinationSelected(item, navController); } }); ......}在这里可以看到,给bottomNavigationView设置了一个item点击事件,进到onNavDestinationSelected方法,
public static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController) { NavOptions.Builder builder = new NavOptions.Builder() .setLaunchSingleTop(true) .setEnterAnim(R.animator.nav_default_enter_anim) .setExitAnim(R.animator.nav_default_exit_anim) .setPopEnterAnim(R.animator.nav_default_pop_enter_anim) .setPopExitAnim(R.animator.nav_default_pop_exit_anim); if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) { builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false); } NavOptions options = builder.build(); try { //TODO provide proper API instead of using Exceptions as Control-Flow. navController.navigate(item.getItemId(), null, options); return true; } catch (IllegalArgumentException e) { return false; }}还记得上篇介绍过的,怎么从一个页面跳转到另一个页面的吗,这里也一样,其实最终就是调用到了navController.navigate()方法进行页面切换的。
使用Navigation+BottomNavigationView结合navigationUI扩展库,这种方式是不是相比于以往的实现方式更简单?可能大家迫不及待的想应用到自己的项目中去了,可殊不知还有坑在里面。
分别在三个fragment中的主要生命周期中打印各自的log,运行程序,打开FragmentHome,可以看到生命周期是正常执行的
然后点击底部的发现切换到FragmentDiscover,FragmentDiscover生命周期也是正常的,但却发现FragmentHome回调了onDestoryView()方法,
再次点击首页切回到FragmentHome,神奇的事情发生了,原来的FragmentHome销毁了,却又重新创建了一个新的FragmentHome实例,即fragment的重绘,并且从log日志中也可以看到,刚刚打开的FragmentDiscover也执行了onDestory同样也销毁了。
下面从源码角度分析为什么会这样。
从NavHostFragment入手,首先看到它的oncreate方法中,
@CallSuper@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) { ...... mNavController = new NavHostController(context); ...... onCreateNavController(mNavController); ...... }去掉无关代码,只看核心代码,可以看到,有一个NavHostController类型的mNavController成员变量,mNavController就是前篇文章中提到的管理导航的navController对象,只不过它是继承自NavController的,戳进去构造方法,发现调用了父类的构造方法,再戳进去来到了NavController的构造方法,
public NavController(@NonNull Context context) { mContext = context; ....... mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider)); mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));}在构造方法中,mNavigatorProvider添加了两个navigator,首先看看mNavigatorProvider是个什么东东,
public class NavigatorProvider { private static final HashMap<Class<?>, String> sAnnotationNames = new HashMap<>();...... @NonNull static String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) { String name = sAnnotationNames.get(navigatorClass); if (name == null) { Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class); name = annotation != null ? annotation.value() : null; if (!validateName(name)) { throw new IllegalArgumentException("No @Navigator.Name annotation found for " + navigatorClass.getSimpleName()); } sAnnotationNames.put(navigatorClass, name); } return name; }}看核心的一个方法getNameForNavigator,该方法传入一个继承了Navigator的类,然后获取其注解为Navigator.Name的值,并通过sAnnotationNames缓存起来,这说起来好像有点抽象,我们看具体的,前面有说到mNavigatorProvider添加了两个navigator,分别是NavGraphNavigator和ActivityNavigator,我们戳进去ActivityNavigator源码,
getNameForNavigator方法对应到这里,其实就是获取到了Navigator.Name的注解值activity,由此可以知道,mNavigatorProvider调用addNavigator方法,就会缓存key为navigator的类,值为这个类的Navigator.Name注解值。
回到前面的NavHostFragment的onCreate方法中,
@CallSuper@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) { ...... mNavController = new NavHostController(context); ...... onCreateNavController(mNavController); ...... }看完了mNavController的构造函数,继续onCreateNavController方法,
@CallSuperprotected void onCreateNavController(@NonNull NavController navController) { navController.getNavigatorProvider().addNavigator( new DialogFragmentNavigator(requireContext(), getChildFragmentManager())); navController.getNavigatorProvider().addNavigator(createFragmentNavigator());}createFragmentNavigator方法
@Deprecated@NonNullprotected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() { return new FragmentNavigator(requireContext(), getChildFragmentManager(), getContainerId());}可以看到,又继续添加了DialogFragmentNavigator和FragmentNavigator两个navigator,至此总共缓存了四个navigator。
回到NavHostFragment的oncreate方法,继续看后面的代码
@CallSuper@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) { ...... mNavController = new NavHostController(context); ...... onCreateNavController(mNavController); ...... if (mGraphId != 0) { // Set from onInflate() mNavController.setGraph(mGraphId); } else { ...... }}在onInflate()方法中可以看出,mGraphId就是在布局文件中定义NavHostFragment时,通过app:navGraph属性指定的导航资源文件,
跟进setGraph()方法,
public void setGraph(@NavigationRes int graphResId) { setGraph(graphResId, null); } public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) { setGraph(getNavInflater().inflate(graphResId), startDestinationArgs); } public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) { if (mGraph != null) { // Pop everything from the old graph off the back stack popBackStackInternal(mGraph.getId(), true); } mGraph = graph; onGraphCreated(startDestinationArgs); }在第二个重载方法中,通过getNavInflater().inflate方法创建出一个NavGraph对象,传到第三个重载的方法中,并赋值给成员变量mGraph,最后在onGraphCreated方法中将第一个页面显示出来。
由此可见,导航资源文件nav_graph会被解析成一个NavGraph对象,看下NavGraph
public class NavGraph extends NavDestination implements Iterable<NavDestination> { final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();}NavGraph继承了NavDestination,NavDestination其实就是nav_graph.xml中navigation下的一个个节点,也就是一个个页面,NavGraph内部有个集合mNodes,用来保存一组NavDestination。
至此我们具体分析了两个重要的步骤,一个是navigator的,一个是nav_graph.xml是如何被解析并关联到navController,弄清楚这两个步骤,对接下来的分析大有帮助。
还记得前面有分析到,BottomNavigationView是怎么做到页面切换的吗,把上面代码照样搬过来,
public static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController) { NavOptions.Builder builder = new NavOptions.Builder() .setLaunchSingleTop(true) .setEnterAnim(R.animator.nav_default_enter_anim) .setExitAnim(R.animator.nav_default_exit_anim) .setPopEnterAnim(R.animator.nav_default_pop_enter_anim) .setPopExitAnim(R.animator.nav_default_pop_exit_anim); if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) { builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false); } NavOptions options = builder.build(); try { //TODO provide proper API instead of using Exceptions as Control-Flow. navController.navigate(item.getItemId(), null, options); return true; } catch (IllegalArgumentException e) { return false; }}没错,是通过 navController.navigate这个方法,传入item.getItemId(),由此可以知道,上面提到过的,定义BottomNavigationView时 app:menu属性指定的menu资源文件中,item标签的id和nav_graph中fragment标签的id保持一致的原因了吧,我们继续跟踪,
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions) { navigate(resId, args, navOptions, null);} public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) { ...... @IdRes int destId = resId; ....... NavDestination node = findDestination(destId); ...... navigate(node, combinedArgs, navOptions, navigatorExtras); } private void navigate(@NonNull NavDestination node, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) { ...... Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator( node.getNavigatorName()); Bundle finalArgs = node.addInDefaultArgs(args); NavDestination newDest = navigator.navigate(node, finalArgs, navOptions, navigatorExtras); ...... }可以看到,在第二个重载方法中,通过findDestination方法传入导航到目标页面的id,获得NavDestination对象node,在第三个重载方法中,通过mNavigatorProvider获取navigator,那么这个navigator是什么呢,还记得上面分析的NavHostFragment经过oncreate方法之后,navigatorProvider总共缓存了四个navigator吗, 由于在nav.graph.xml中,定义的是<framgent>标签,所以这里navigator最终拿到的是一个FragmentNavigator对象。进到FragmentNavigator的navigate方法
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) { if (mFragmentManager.isStateSaved()) { Log.i(TAG, "Ignoring navigate() call: FragmentManager has already" + " saved its state"); return null; } String className = destination.getClassName(); if (className.charAt(0) == '.') { className = mContext.getPackageName() + className; } final Fragment frag = instantiateFragment(mContext, mFragmentManager, className, args); frag.setArguments(args); final FragmentTransaction ft = mFragmentManager.beginTransaction(); int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1; int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1; int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1; int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1; if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) { enterAnim = enterAnim != -1 ? enterAnim : 0; exitAnim = exitAnim != -1 ? exitAnim : 0; popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0; popExitAnim = popExitAnim != -1 ? popExitAnim : 0; ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim); } ft.replace(mContainerId, frag); ft.setPrimaryNavigationFragment(frag); final @IdRes int destId = destination.getId(); final boolean initialNavigation = mBackStack.isEmpty(); // TODO Build first class singleTop behavior for fragments final boolean isSingleTopReplacement = navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && mBackStack.peekLast() == destId; boolean isAdded; if (initialNavigation) { isAdded = true; } else if (isSingleTopReplacement) { // Single Top means we only want one instance on the back stack if (mBackStack.size() > 1) { // If the Fragment to be replaced is on the FragmentManager's // back stack, a simple replace() isn't enough so we // remove it from the back stack and put our replacement // on the back stack in its place mFragmentManager.popBackStack( generateBackStackName(mBackStack.size(), mBackStack.peekLast()), FragmentManager.POP_BACK_STACK_INCLUSIVE); ft.addToBackStack(generateBackStackName(mBackStack.size(), destId)); } isAdded = false; } else { ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId)); isAdded = true; } if (navigatorExtras instanceof Extras) { Extras extras = (Extras) navigatorExtras; for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) { ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue()); } } ft.setReorderingAllowed(true); ft.commit(); // The commit succeeded, update our view of the world if (isAdded) { mBackStack.add(destId); return destination; } else { return null; }}通过Destination拿到ClassName,instantiateFragment方法通过内反射创建出对应的fragment,最后通过FragmentTransaction的replace方法创建fragment。
至此,终于真相大白了!我们知道replace方法每次都会重新创建fragment,所以使用Navigation创建的底部导航页面,每次点击切换页面当前fragment都会重建。
既然知道了fragment重绘的原因,那就可以对症下药了,我们知道,fragment的切换除了replace,还可以通过hide和show,那怎么做到呢,通过前面的分析,其实可以自定义一个navigator继承FragmentNavigator,重写它的navigate方法,从而达到通过hide和show进行fragment切换的目的。
这里新建一个FixFragmentNavigator类,我们希望在nav_graph中通过fixFragment标签来指定每个导航页面
@Navigator.Name("fixFragment")class FixFragmentNavigator(context: Context, manager: FragmentManager, containerId: Int) : FragmentNavigator(context, manager, containerId) { private val mContext = context private val mManager = manager private val mContainerId = containerId private val TAG = "FixFragmentNavigator" override fun navigate( destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Navigator.Extras? ): NavDestination? { if (mManager.isStateSaved) { Log.i(TAG, "Ignoring navigate() call: FragmentManager has already" + " saved its state") return null } var className = destination.className if (className[0] == '.') { className = mContext.packageName + className } val ft = mManager.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) } /** * 1、先查询当前显示的fragment 不为空则将其hide * 2、根据tag查询当前添加的fragment是否不为null,不为null则将其直接show * 3、为null则通过instantiateFragment方法创建fragment实例 * 4、将创建的实例添加在事务中 */ val fragment = mManager.primaryNavigationFragment //当前显示的fragment if (fragment != null) { ft.hide(fragment) ft.setMaxLifecycle(fragment, Lifecycle.State.STARTED); } var frag: Fragment? val tag = destination.id.toString() frag = mManager.findFragmentByTag(tag) if (frag != null) { ft.show(frag) ft.setMaxLifecycle(frag, Lifecycle.State.RESUMED); } else { frag = instantiateFragment(mContext, mManager, className, args) frag.arguments = args ft.add(mContainerId, frag, tag) } ft.setPrimaryNavigationFragment(frag) @IdRes val destId = destination.id /** * 通过反射的方式获取 mBackStack */ val mBackStack: ArrayDeque<Int> val field = FragmentNavigator::class.java.getDeclaredField("mBackStack") field.isAccessible = true mBackStack = field.get(this) as ArrayDeque<Int> val initialNavigation = mBackStack.isEmpty() val isSingleTopReplacement = (navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && mBackStack.peekLast() == destId) val isAdded: Boolean if (initialNavigation) { isAdded = true } else if (isSingleTopReplacement) { // Single Top means we only want one instance on the back stack if (mBackStack.size > 1) { // If the Fragment to be replaced is on the FragmentManager's // back stack, a simple replace() isn't enough so we // remove it from the back stack and put our replacement // on the back stack in its place mManager.popBackStack( zygoteBackStackName(mBackStack.size, mBackStack.peekLast()), FragmentManager.POP_BACK_STACK_INCLUSIVE ) ft.addToBackStack(zygoteBackStackName(mBackStack.size, destId)) } isAdded = false } else { ft.addToBackStack(zygoteBackStackName(mBackStack.size + 1, destId)) isAdded = true } if (navigatorExtras is Extras) { val extras = navigatorExtras as Extras? for ((key, value) in extras!!.sharedElements) { ft.addSharedElement(key, value) } } ft.setReorderingAllowed(true) ft.commit() // The commit succeeded, update our view of the world if (isAdded) { mBackStack.add(destId) return destination } else { return null } } private fun zygoteBackStackName(backIndex: Int, destid: Int): String { return "$backIndex - $destid" }}新建一个导航资源文件fix_nav_graph.xml,将原本的fragment换成fixFragment
<?xml version="1.0" encoding="utf-8"?><navigation xmlns:andro xmlns:app="http://schemams.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android: app:startDestination="@id/FragmentHome"> <fixFragment android: android:name="com.example.testnavigation.FragmentHome" android:label="fragment_home" tools:layout="@layout/fragment_home"> </fixFragment> <fixFragment android: android:name="com.example.testnavigation.FragmentDicover" android:label="fragment_discover" tools:layout="@layout/fragment_discover"> </fixFragment> <fixFragment android: android:name="com.example.testnavigation.FragmentMine" android:label="fragment_mine" tools:layout="@layout/fragment_mine"> </fixFragment></navigation>然后把activity_main.xml中的app:navGraph属性值替换为fix_nav_graph,
“修复版的”FragmentNavigator写好后,在MainActivity中,通过navController把它添加到fragmentNavigator中,
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navController = Navigation.findNavController(this, R.id.fragment) val fragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment val fragmentNavigator = FixFragmentNavigator(this, supportFragmentManager, fragment.id) //添加自定义的FixFragmentNavigator navController.navigatorProvider.addNavigator(fragmentNavigator) bottom_nav_view.setupWithNavController(navController) }满心欢喜的以为大功告成了,运行程序发现崩了,报错如下:
报错信息很明显,找不到fixFragment对应的navigator,必须通过addNavigator方法进行添加,这怎么回事呢?明明已经调用addNavigator方法添加自定义的FixFragmentNavigator了。别急,还是回到NavHostFragment的onCreate()方法中,
@CallSuper@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) { ...... if (mGraphId != 0) { // Set from onInflate() mNavController.setGraph(mGraphId); } else { // See if it was set by NavHostFragment.create() final Bundle args = getArguments(); final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0; final Bundle startDestinationArgs = args != null ? args.getBundle(KEY_START_DESTINATION_ARGS) : null; if (graphId != 0) { mNavController.setGraph(graphId, startDestinationArgs); } }}上面已经说过了mGraphId就是通过app:navGraph指定的导航资源文件,那么mGraphId此时不等于0,走到if语句中,
@CallSuperpublic void setGraph(@NavigationRes int graphResId) { setGraph(graphResId, null);}@CallSuperpublic void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) { setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);}进到getNavInflater().inflate
@SuppressLint("ResourceType")@NonNullpublic NavGraph inflate(@NavigationRes int graphResId) { ...... try { ...... NavDestination destination = inflate(res, parser, attrs, graphResId); if (!(destination instanceof NavGraph)) { throw new IllegalArgumentException("Root element <" + rootElement + ">" + " did not inflate into a NavGraph"); } return (NavGraph) destination; } catch (Exception e) { throw new RuntimeException("Exception inflating " + res.getResourceName(graphResId) + " line " + parser.getLineNumber(), e); } finally { parser.close(); }}进到inflate方法,
@NonNullprivate NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser, @NonNull AttributeSet attrs, int graphResId) throws XmlPullParserException, IOException { Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName()); ......}进到getNavigator方法
@CallSuper@NonNullpublic <T extends Navigator<?>> T getNavigator(@NonNull String name) { if (!validateName(name)) { throw new IllegalArgumentException("navigator name cannot be an empty string"); } Navigator<? extends NavDestination> navigator = mNavigators.get(name); if (navigator == null) { throw new IllegalStateException("Could not find Navigator with name "" + name + "". You must call NavController.addNavigator() for each navigation type."); } return (T) navigator;}原来报错的信息在这里,这里其实就是通过标签获取对应的navigator,然而在NavHostFragmen执行oncreate后,默认只添加了原本的四个navigator,而此时在解析fixFragment节点时,我们自定义的FixFragmentNavigator还未添加进来,所以抛了这个异常。
那么我们是不能在布局文件中通过app:navGraph属性指定自定义的导航资源文件了,只能在布局文件中去掉app:navGraph这个属性,然后在添加FixFragmentNavigator的同时,通过代码将导航资源文件设置进去。
最终代码如下:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navController = Navigation.findNavController(this, R.id.fragment) val fragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment val fragmentNavigator = FixFragmentNavigator(this, supportFragmentManager, fragment.id) //添加自定义的FixFragmentNavigator navController.navigatorProvider.addNavigator(fragmentNavigator) //通过代码将导航资源文件设置进去 navController.setGraph(R.navigation.fix_nav_graph) bottom_nav_view.setupWithNavController(navController) }运行程序,观察各fragment的生命周期,发现已经不会重新走生命周期了。
本篇在上篇的基础上,结合BottomNavigationView实现了第一个底部导航切换的实例,然后介绍了这种方式引发的坑,进而通过源码分析了发生这种现象的原因,并给出了解决的思路。读懂源码才是最重要的,现在再总结一下navigator进行页面切换的原理: