前段时间用单Activity架构写完了项目,在整个过程中遇到了很多问题,现在记录一下,避免再次踩坑。
首先对项目做一个概述:
整个app只有一个MainActivity,首次进入app显示登入注册相关Fragment页面,登入后进入MainFragment页面,MainFragment布局由ViewPager和5个RadioButton构成,ViewPager承载5个fragment,RadioButton用于切换fragment;
项目中封装了BaseFragment处理一些常用的操作,所有Fragment继承于BaseFragment。
1、Fragment的跳转使用的是transaction.replace方法,如果添加到返回栈,当前Fragment不会被remove,只会onDestroyView销毁view,下次显示时重新onCreateView创建view;如果不添加到返回栈,当前Fragment会被remove销毁掉;Fragment的返回使用的是fragmentManager.popBackStack(String name, int flags)方法。
popBackStack(String name, int flags)参数的意义:
name为null,flags为0,弹出栈中最上层的fragment;
name为null,flags为1,弹出栈中所有的fragment;
name不为null,flags为0,弹出栈中该Fragment之上的Fragment;
name不为null,flags为1,弹出栈中该Fragment和之上的Fragment;
name为添加到返回栈中的name;
protected final void replaceFragment(Fragment fragment, boolean isAddToBackStack) {
if (getActivity() == null) return;
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.fragment_contains, fragment);
if (isAddToBackStack) {
transaction.addToBackStack(fragment.getClass().getName());
}
transaction.commit();
}
2、APP运行时退到后台,当内存紧张或者APP权限被修改后,MainActivity可能会被回收,如果被回收之后,再次将app放在前台:
(1)、MainActivity会重建,onCreate方法中的savedInstanceState不为空,并且会恢复FragmentManager之前的状态及返回栈,所以onCreate中需要判断savedInstanceState是否为空,如果为空就添加Fragment,如果不为空就不处理,由系统恢复Fragment即可;
(2)、在重建过程中,先调用栈顶Fragment以及它的目标Fragment的onCreate,然后再调用Activity的onCreate,而不是先调用Activity的onCreate;
(3)、Fragment中的数据要在onSaveInstanceState方法中保存,在onCreate中将数据恢复;
(4)、Activity中的数据要在onSaveInstanceState方法中保存,在onCreate或onRestoreInstanceState中将数据恢复;如果Fragment中有访问Activity中的对象,需要考虑Fragment访问的对象是否已经初始化或者恢复数据,否则出现空指针或者bug;
3、为方便Fragment处理返回键事件,以及点击两次返回键退出程序,重写了MainActivity的onBackPressed方法:
(1)首先是通过FragmentManager获取当前显示的Fragment,如果属于BaseFragment,就调用它的onBackPressed,并返回boolean值,如果为ture,表示处理完毕,如果返回false,继续由onBack方法处理;
public void onBackPressed() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_contains);
if (fragment instanceof BaseFragment) {
if (((BaseFragment) fragment).onBackPressed()) {
return;
} else {
onBack();
}
} else {
onBack();
}
}
getSupportFragmentManager().findFragmentById方法的是查找add列表里面最顶上的那个Fragment,因为页面切换使用的是replace方法,所以add列表里面只会有当前显示的Fagment;
(2)onBack方法中查询FragmentManager中的返回栈中Fragment数量,如果是0表示当前只有一个页面:
private long time = 0;
public void onBack() {
FragmentManager manager = getSupportFragmentManager();
if (manager.getBackStackEntryCount() == 0) {
long current = System.currentTimeMillis();
if ((current - time) > 2000) {
time = current;
GoodToast.show(R.string.exit_app, Toast.LENGTH_SHORT);
} else {
super.onBackPressed();
}
} else {
super.onBackPressed();
}
}
4、MainFragment中的ViewPager,使用的FragmentPagerAdapter,它和FragmentStatePagerAdapter有一些区别,体现在切换Fragment时:
FragmentStatePagerAdapter 会销毁不需要的Fragment,并从FragmentManager中移除,同时会调用onSaveInstanceState方法保存数据,下次显示时可以恢复数据;
FragmentPagerAdapter只会销毁Fragment的视图,onDestroyView会被调用,Fragment的实例还会保存在FragmentManager中,下次显示时会走onCreateView方法重新创建view;
两者相比FragmentStatePagerAdapter更节省内存,适用tab比较多的情况。
5、MainFragment中,PagerAdapter的FragmentManager要使用Fragment的FragmentManager,通过getChildFragmentManager()获得,而不能使用getActivity().getSupportFragmentManager();
getChildFragmentManager()区别于getActivity().getSupportFragmentManager(),因为在Fragment中使用ViewPager显示多个Fragment,所以需要使用fragment中的FragmentManager;
如果使用getActivity().getSupportFragmentManager(),在ViewPager中的Fragment打开另一个Fragment时有两个问题:
(1)、两个Fragment的onCreateOptionsMenu都会被调用,从而导致被打开的Fragment的Toolbar菜单按钮点击事件失效,且会一起显示出来;
(2)、返回时,ViewPager中的Fragment的视图会为空,显示空白;
6、transaction.setCustomAnimations(anim0, anim1, anim2, anim3)用来设置转场动画,四个参数的分别为:
anim0:要打开的页面的动画
anim1:打开页面时被关闭的页面的动画
anim2:返回后要重新显示的页面的动画
anim3:返回时要关闭的页面的动画
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)设置转场动画为淡入淡出
2019/11/21 注:
1、setCustomAnimations需要在调用add,replace,show,hide方法之前设置,不然不起作用;
2、要添加一个新的fragment,并且显示出来,只需要hide当前显示的fragment,add新的fragment即可,不用再调用一次show方法。当然如果没有设置转场动画的话,调用show也没有关系。但如果有setTransition,且show方法在add方法的后面调用,你会发现跳转后返回时,被关闭的fragment没有执行动画就消失了。这时只要去掉show方法或者把show方法放到add的前面即可。
7、在MainFragment的其中一个Fragment里打开另一个Fragment时,如果通过transaction.setCustomAnimations(anim0, anim1, anim2, anim3)设置了转场动画,不会看到被关闭Fragment的动画,显示的是主题中的windowBackground颜色,而返回时却可以正常显示动画;可能是因为不同由FragmentManager管理的原因;(2019/11/21 注:可能是上面说的原因,调用了add后有调用了show)
8、在两个Fragment之间,一个Fragment跳转到另一个Fragement做一些处理,处理完后返回并要告知上一个Fragment处理结果,一般有两种方法:
(1)、通过接口回调;
(2)、给做处理工作的Fragment设置目标Fragment,处理完成后调用它目标Fragment的onActivityResult方法告知处理结果,但是这里会有一个容易出问题的地方,如果这两个Fraggment的不是由同一个FragmentManager管理,会报下面异常:
java.lang.IllegalStateException: Fragment * declared target fragment * that does not belong to this FragmentManager!
9、当Fragment在处理耗时任务时,把app退到后台,此时会调用Fragment的onSaveInstanceState方法,且FragmentManager中的mStateSaved变为true,如果在耗时任务处理完成后app仍在后台,而又操作了返回栈(跳转页面),FragmentManager判断mStateSaved如果为true,会报下面异常,导致app闪退:
java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState;
解决方法
(1)、对于添加新的Fragment可以使用transaction.commitAllowingStateLoss()来代替transaction.commit()来避免这个异常;
(2)、对于页面返回,调用FragmentManager.popBackStack()或者getActivity().onBackPressed(),必须要判断isStateSaved()的值,为true时不能进行页面返回,为false时才能返回;
(3)、isStateSaved()的返回值在onSaveInstanceState调用时为true,在onResume时为false;
10、假设有3个Fragment,A、B、C,A跳转到B,B再跳转到C,C返回时要直接返回到A,
如果A跳转到B时,将A添加到返回栈,B跳转到C时,不将B添加到返回栈,然后在C中返回,虽然是直接返回到了A,但是C不会被FragmentManager移除,也不会被销毁;
正确的处理方式是将A,B都添加到返回栈中transaction.addToBackStack(fragment.getClass().getName()),C返回时使用FragmentManager的popBackStack(String name, int flags)方法将B、C出栈;
11、实现ViewPager中Fragment的懒加载:
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && getView() != null) {
lazyLoading();
}
}
protected void lazyLoading() {
}
@Override
public void onResume() {
super.onResume();
if (getUserVisibleHint()) {
lazyLoading();
}
}
setUserVisibleHint 方法的调用时机是fragment被切换到时,逻辑很简单,判断fragment如果可见并且view已经创建,就会调用lazyLoading();
假设ViewPager的OffscreenPageLimit为1,会预加载两个Fragment,加载过程中调用顺序是setUserVisibleHint—onCreateVeiw—onResume,因为view还没有被创建,所以两个fragment的lazyLoading()方法不会被调用,但是选中的fragment会在view被创建之后再次调用setUserVisibleHint方法,此时lazyLoading()方法就会被调用;另一个未选中的fragment在被选中时同样也会调用setUserVisibleHint方法,因为在预加载过程中view已经创建,所以lazyLoading()会被调用到;
如果当前显示第一个Fragment,点击第三个Fragment,因为第三个Fragment的视图未创建,getView为null,所以setUserVisibleHint方法里面的lazyLoading不会被执行,但是在onResume中,getUserVisibleHint此时返回true会去执行lazyloading。
2022/09/07注:
上面的描述不是很清晰,可能有错误,但代码是没有问题的。今天补充说明下。setUserVisibleHint 和 onResume 方法都会调用 lazyLoading ,但不会出现同时调用的情况,他们分别覆盖不同的场景:
场景1:A、B、C、D 四个Fragment ,默认情况下,会加载 A、B,显示的是A,A 的 setUserVisibleHint 被调用两次,一次为false,一次为true,但两次都是 getView 为空,所以不会执行 lazyLoading ,A 会接着执行 onCreate、onCreateView、onResume,因为 getUserVisibleHint 为true,所以会执行lazyLoading 方法。B 会执行 setUserVisibleHint(false),onCreate、onCreateView、onResume,因为 getUserVisibleHint 为false,所以不会执行 lazyLoading 方法。等到切换到 B 时,B 会执行setUserVisibleHint(true),因为 getView 不为空,所以会执行 lazyLoading 方法。
场景2:还是 A、B、C、D 四个Fragment,跳过B直接切换到C,和场景1中的A类似,会在onResume中执行 lazyLoading 。
当然ViewPager已经废弃了,建议使用ViewPager2,自带懒加载功能。
但是这里还是有点问题,lazyLoading()并不是只会在fragment第一次显示时执行,当fragment再次显示时,lazyLoading()仍然会被调用,所以需要在lazyLoading中判断如果数据为空时才请求数据;