业余 Android:SDK 自带 Navigation 导航栏切换 Fragment 不复用问题 时间: 2021-01-03 15:38 分类: JAVA ####前言 这里我是使用`Android Studio`创建的一个默认侧滑导航工程,使用的时候会发现切换导航`Fragment`的时候,数据会丢失需要重新加载,也就是`Fragment`的`onCreate`会反复调用。 而实际很多时候我们需要的是切换导航数据不进行刷新重新加载,也就是保持原先的状态。 ####解决 由于是业余,`Android Studio`创建的默认代码看起来有点懵逼,因为代码里就只创建了一个`NavController`,对`Fragment`的切换代码全部封装了起来,根本无从下手,于是直接网上搜索解决方案,找到问题缘由: > Fragment 的切换时通过 FragmentNavigator 类的 navigate 方法来进行控制的,而里面的切换是用 FragmentTransaction.replace 来实现的,这也就难免会出现重复创建 Fragment 的问题。 知道了问题所在,那么解决方案也就一并出来了,那就是重写`navigate`方法,将里面的`replace`改成`show`与`hide`的切换,具体代码网上直接 copy 了一份如下: ```kotlin @Navigator.Name("fragment") public class FixFragmentNavigator extends FragmentNavigator { private static final String TAG = "FixFragmentNavigator"; private Context mContext; private FragmentManager mFragmentManager; private int mContainerId; public FixFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) { super(context, manager, containerId); mContext = context; mFragmentManager = manager; mContainerId = containerId; } @Nullable @Override 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; } //fix 1: 把类名作为tag,寻找已存在的Fragment //(如果想只针对个别fragment进行保活复用,可以在tag上做些标记比如加个前缀) Fragment frag = mFragmentManager.findFragmentByTag(className); if (null == frag) { //不存在,则创建 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); //fix 2: replace换成show和hide List fragments = mFragmentManager.getFragments(); for (Fragment fragment : fragments) { ft.hide(fragment); } if (!frag.isAdded()) { ft.add(mContainerId, frag, className); } ft.show(frag); ft.setPrimaryNavigationFragment(frag); final @IdRes int destId = destination.getId(); //fix 3: mBackStack是私有的,而且没有暴露出来,只能反射获取 ArrayDeque mBackStack; try { Field field = FragmentNavigator.class.getDeclaredField("mBackStack"); field.setAccessible(true); mBackStack = (ArrayDeque) field.get(this); } catch (Exception e) { e.printStackTrace(); return null; } final boolean initialNavigation = mBackStack.isEmpty(); final boolean isSingleTopReplacement = navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && mBackStack.peekLast() == destId; boolean isAdded; if (initialNavigation) { isAdded = true; } else if (isSingleTopReplacement) { if (mBackStack.size() > 1) { 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 sharedElement : extras.getSharedElements().entrySet()) { ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue()); } } ft.setReorderingAllowed(true); ft.commit(); if (isAdded) { mBackStack.add(destId); return destination; } else { return null; } } //fix 4: 从父类那边copy过来即可 @NonNull private String generateBackStackName(int backStackIndex, int destId) { return backStackIndex + "-" + destId; } } ``` 重写之后该怎么办呢? 查看`NavigatorProvider.addNavigator`方法可以发现内部其实就是个`Map`,添加就是`put`操作。 因此其实可以完全没必要给自定义的`添加自定义的FixFragmentNavigator`设置一个别名,直接用默认的`@Navigator.Name("fragment")`即可,这样一来就可以将原来的直接覆盖掉。 如果想直接使用默认自动创建的代码,那么需要将`navGraph`设置为手动,也就是将布局文件中`Fragment`的`app:navGraph="@navigation/mobile_navigation"`属性去掉,如果不去掉的话,将会在`sentConetntView`的时候就会初始化菜单,此时内部代码创建的是`FragmentNavigator`,如果我们后面再使用`NavigatorProvider.addNavigator`进行替换的话,会有两个问题: 1. `sentConetntView`的时候会创建首页`Fragment`,`FragmentNavigator`里的`mBackStack`会有一个值,我们后面自定义的`FixFragmentNavigator`新建时`mBackStack`是空的,就需要利用反射将原先的`mBackStack`复制过来。 2. `sentConetntView`创建的首页`Fragment`是没有设置`tag`的,第一次从其他导航切换回首页会发生二次创建`首页Fragment`(因为首次创建的时候没有设置tag),解决办法就是`FragmentTransaction.add`方法将`tag`设置进去。 解决上面两个问题就可以了吗?答案是`NO`,关键的地方还不是上面问题,最关键的是`NavigationItemSelectedListener`事件,默认创建的导航有如下代码: > navView.setupWithNavController(navController) 里面坑爹的是`NavigationItemSelectedListener`切换导航事件会把栈顶的`Fragment`清除掉!!最终导致的问题就是`Fragment`重新创建。 我们需要做的就是替换导航切换事件: ``` navView.setNavigationItemSelectedListener { navController.navigate(it.itemId) val parent: ViewParent = navView.parent if (parent is Openable) { (parent as Openable).close() } return@setNavigationItemSelectedListener true } ``` 最后注意重写`返回键`事件,因为我们没有对回退栈做特殊处理,使用的时候会出问题,简单处理就是重写`返回键`事件,让它回到首页: ``` override fun onBackPressed() { if(navController.currentDestination!!.id != R.id.nav_home) navController.navigate(R.id.nav_home) else super.onBackPressed() } ``` 至此问题才能得到解决。 ####结束语 如果按照网上那篇文章解决办法,也是可行的,只不过他的`NavGraph`是用代码手动创建的,其实没必要,官方例子代码是以试图配置文件形式创建的:`navController.setGraph(R.navigation.mobile_navigation)` 最关键的还是`setNavigationItemSelectedListener`事件。无论是有没有使用`navView.setupWithNavController(navController)`,都需要编写导航切换事件,只不过`navView.setupWithNavController(navController)`里面的`setNavigationItemSelectedListener`有坑,那就是切换导航会把栈中的`Fragment`清除掉导致重建,因此我们需要重写替换。 标签: 无