一、出现的现象
使用Navigation进行Fragment之间的跳转,在回退的时候发现之前的Fragment页面上的数据不在了,经排查发现是页面被重新执行了onCreateView,也就是说页面重新被创建了,这样的话跟之前的多Activity的体验效果就不一样了。
二、分析原因
其实这是google故意这么设计的,google的本意是在宿主Activity里有ViewModel,ViewModel维护着页面里的数据,Fragment每次被创建就通过ViewModel获取之前的数据。如果遇到页面是ListView这种,如何在重新创建的时候保持之前的状态就比较麻烦了。那么有什么方法可以解决呢?
三、解决方法
首先我们要知道为何返回的时候Fragment执行了onCreateView,经过分析关键在于FragmentNavigator这个类中的navigate方法
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
//忽略部分代码
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);
}
//关键在于这个replace,导致每次打开新页面都会把当前的页面给替换了
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;
//忽略部分代码
}
经过源码的查看其实就是打开的新页面把当前的页面给替换了,我们知道可以通过先hide当前页面然后在add新页面不就行了,但是源码我们无法去修改,因为我们可以复制一份自己改,在引用的时候用自己创建的这个类不就行了。
我们拷贝FragmentNavigator类重命名一个MyFragmentNavigator,修改navigate方法
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
//忽略部分代码
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);
}
//先把当前的fragment隐藏,然后add新页面
if(mFragmentManager.getFragments().size()>0){
ft.hide(mFragmentManager.getFragments().get(mFragmentManager.getFragments().size()-1));
ft.add(mContainerId, frag);
}else {
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;
//忽略部分代码
}
目前我们创建了MyFragmentNavigator,但是这个MyFragmentNavigator没法直接使用,通过源码分析得知NavHostFragment引用了FragmentNavigator,那么我们肯定也要修改一下NavHostFragment,我们只需要复制一份NavHostFragment重命名为MyNavHostFragment,把里面引用的FragmentNavigato改为我们自己创建的MyFragmentNavigator就可以了。
四、如何使用
我们知道在Activity布局文件里是这样写的
<fragment
app:defaultNavHost="true"
app:navGraph="@navigation/my_nav"
android:id="@+id/nav_host_fragment"
//把这里改成MyNavHostFragment的全路径就可以了
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
</fragment>
然后我们就可以和原来的正常使用就行了,页面的跳转传参都用之前的代码
总结:
1.复制FragmentNavigator为MyFragmentNavigator,修改里面的navigate方法
2.复制NavHostFragment为MyNavHostFragment,将内部引用的FragmentNavigator改为MyFragmentNavigator
3.在主Activity布局文件里替换为MyNavHostFragment的全路径