写在前面
Fragment的懒加载,在我看来就是为了抵抗ViewPager的预加载机制所做的抵抗。这样会使得用户不需要在一开始就加载好之后的内容,可以节约资源,也可以避免网络堵塞,增强用户体验。
一、名词解释
ViewPager为了让滑动的时候可以有很好的用户的体验,也就是防止出现卡顿现象,因此它有一个缓存机制。默认情况下,ViewPager会提前创建好当前Fragment旁的两个Fragment,举个例子说也就是如果你当前显示的是编号3的Fragment,那么其实编号2和4的Fragment也已经创建好了,也就是说这3个Fragment都已经执行完 onAttach() -> onResume() 这之间的生命周期函数了。
什么是懒加载,就是只用到了该用它的时候它才加载。只有当Fragment被切换到了当前页面的时候,才让它去请求数据。
二、实现具体思路
在这个懒加载需求的面前,我们很容易就想到,如果有一个方法,他可以做到在Fragment呈现到我们面前的时候才会去加载数据的话,那么就可以直接完成这个需求。在我不懈的百度之下,我发现了下面这个方法。
setUserVisibleHint(boolean isVisibleToUser)
传闻说,只要将加载数据的操作放到这个里面就可以实现我们的需求,我们来试试。结果如下图所示:
这里0MainClidFragment却先打出了false,然后才打出true,这是因为setUserVisibleHint()在Fragment实例化时会先调用一次,并且默认值是false,当选中当前显示的Fragment时还会再调用一次。
预加载会使0和1位置的Fragment加载到ViewPager中去;而这个方法可以使当前显示到用户面前的时候才会显示为true;那么按理来说就已经基本实现了我们所想要的懒加载了。如果只是为了实现数据加载的话,现在就已经实现了。
但在实际开发过程中,实际上我们还需要进行一些控件的操作,大概是:
1.在Fragment可见时显示控件,例如:显示加载控件;
2.在Fragment从可见到不可见时取消控件的显示,例如:取消加载控件的显示;
这里说明一下原因;为什么在Fragment可见时显示控件通过这个方法不一定能成功呢?因为setUserVisibleHint()可能会在Fragment的生命周期之外被调用,也就是可能在view创建前就被调用,也可能在destroyView后被调用,所以如果涉及到一些控件的操作的话,可能会报 null 异常,因为控件还没初始化,或者已经摧毁了。
所以,我继续在网上找到了一种关于懒加载的写法,该题主先后改过两次,我们直接来看他完成之后的一个版本。他在封装好一个懒加载的基类之后具体实现了下面几个功能。
一.支持数据的懒加载且只加载一次;
这一点是常用的一点,我们不能在onCreate()或者onCreateView方法中直接下载数据,因为这样就会直接根据ViewPager的预加载处理机制来进行处理,就不会有懒加载的效果;而且也必须考虑是不是第一次进入Fragment页面,如果是第一次进入这个页面,那么我们就加载数据,如果不是第一次,就呈现上次加载的数据;
二.只有两种情况会触发该函数(支持你在这里进行一些 ui 操作,如显示/隐藏加载框):
1、一种是Fragment从“不可见 -> 可见” 时触发,并传入 isVisible = true
2、一种是Fragment从“可见 -> 不可见” 时触发,并传入 isVisible = false
因为我们之前说过,setUserVisibleHint()方法很可能在onCreateView方法之前或者onDestroyView之后调用,这个时候我们还没有进行控件View的初始化或者已经销毁控件。所以,我们必须进行一些判断,确保控件已经创建完成且没有销毁。
并且,我们需要给出加载控件的显示和取消就需要把加载控件的呈现与否放置到Fragment从可见到不可见的判断中去,且需要在ui控件已经创建成功之后触发。这样才能对ui进行操作。
三.支持 view 的复用,防止与 ViewPager 使用时出现重复创建 view 的问题。
下面就是该题主封装之后的BaseFragment代码:
/**
* Created by dasu on 2016/9/27.
*
* Fragment基类,封装了懒加载的实现
*
* 1、Viewpager + Fragment情况下,fragment的生命周期因Viewpager的缓存机制而失去了具体意义
* 该抽象类自定义新的回调方法,当fragment可见状态改变时会触发的回调方法,和 Fragment 第一次可见时会回调的方法
*
* @see #onFragmentVisibleChange(boolean)
* @see #onFragmentFirstVisible()
*/
public abstract class BaseFragment extends Fragment {
private static final String TAG = BaseFragment.class.getSimpleName();
private boolean isFragmentVisible;
private boolean isReuseView;
private boolean isFirstVisible;
private View rootView;
//setUserVisibleHint()在Fragment创建时会先被调用一次,传入isVisibleToUser = false
//如果当前Fragment可见,那么setUserVisibleHint()会再次被调用一次,传入isVisibleToUser = true
//如果Fragment从可见->不可见,那么setUserVisibleHint()也会被调用,传入isVisibleToUser = false
//总结:setUserVisibleHint()除了Fragment的可见状态发生变化时会被回调外,在new Fragment()时也会被回调
//如果我们需要在 Fragment 可见与不可见时干点事,用这个的话就会有多余的回调了,那么就需要重新封装一个
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
//setUserVisibleHint()有可能在fragment的生命周期外被调用
if (rootView == null) {
return;
}
if (isFirstVisible && isVisibleToUser) {
onFragmentFirstVisible();
isFirstVisible = false;
}
if (isVisibleToUser) {
onFragmentVisibleChange(true);
isFragmentVisible = true;
return;
}
if (isFragmentVisible) {
isFragmentVisible = false;
onFragmentVisibleChange(false);
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initVariable();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
//如果setUserVisibleHint()在rootView创建前调用时,那么
//就等到rootView创建完后才回调onFragmentVisibleChange(true)
//保证onFragmentVisibleChange()的回调发生在rootView创建完成之后,以便支持ui操作
if (rootView == null) {
rootView = view;
if (getUserVisibleHint()) {
if (isFirstVisible) {
onFragmentFirstVisible();
isFirstVisible = false;
}
onFragmentVisibleChange(true);
isFragmentVisible = true;
}
}
super.onViewCreated(isReuseView ? rootView : view, savedInstanceState);
}
@Override
public void onDestroyView() {
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
initVariable();
}
private void initVariable() {
isFirstVisible = true;
isFragmentVisible = false;
rootView = null;
isReuseView = true;
}
/**
* 设置是否使用 view 的复用,默认开启
* view 的复用是指,ViewPager 在销毁和重建 Fragment 时会不断调用 onCreateView() -> onDestroyView()
* 之间的生命函数,这样可能会出现重复创建 view 的情况,导致界面上显示多个相同的 Fragment
* view 的复用其实就是指保存第一次创建的 view,后面再 onCreateView() 时直接返回第一次创建的 view
*
* @param isReuse
*/
protected void reuseView(boolean isReuse) {
isReuseView = isReuse;
}
/**
* 去除setUserVisibleHint()多余的回调场景,保证只有当fragment可见状态发生变化时才回调
* 回调时机在view创建完后,所以支持ui操作,解决在setUserVisibleHint()里进行ui操作有可能报null异常的问题
*
* 可在该回调方法里进行一些ui显示与隐藏,比如加载框的显示和隐藏
*
* @param isVisible true 不可见 -> 可见
* false 可见 -> 不可见
*/
protected void onFragmentVisibleChange(boolean isVisible) {
}
/**
* 在fragment首次可见时回调,可在这里进行加载数据,保证只在第一次打开Fragment时才会加载数据,
* 这样就可以防止每次进入都重复加载数据
* 该方法会在 onFragmentVisibleChange() 之前调用,所以第一次打开时,可以用一个全局变量表示数据下载状态,
* 然后在该方法内将状态设置为下载状态,接着去执行下载的任务
* 最后在 onFragmentVisibleChange() 里根据数据下载状态来控制下载进度ui控件的显示与隐藏
*/
protected void onFragmentFirstVisible() {
}
protected boolean isFragmentVisible() {
return isFragmentVisible;
}
}
对于上述BaseFragment的代码思路,我进行思路梳理:
一.为了实现Fragment中的onCreateView中创建的View复用的目的,在这段代码中使用了onViewCreated,这个是在onCreateView之后触发的事件,onCreateView的返回值传入了onViewCreated,就像最后注意事项中说的那样,如果要完全解决掉ViewPager的View复用问题,就必须在ViewPager的中 destroyItem() 方法,将 super 去掉,也就是不销毁 view。
二、我们来总结一下setUserVisibleHint()方法的触发情况:
1.在Fragment创建的时候会调用第一次,isVisibleToUser = false;
2.在fragment从不可见到可见的时候会触发第二次,isVisibleToUser = true;
3.在Fragment从可见到不可见的时候会触发第三次,isVisibleToUser = false;
在上述这种情况下,我们要保留第二,第三种情况,所以我们使用rootView==null来进行判断,为真时用return结束当前方法。直接排除出第一种情况。两种情况保留成功。
三、现在我们来实现最后一个功能,就是将第一次进入Fragment加载数据和其他情况分开讨论。我们在设置一个isFirstVisible的boolean值,用来判断是否是第一次进入Fragment;如果是的话,实现onFragmentFirstVisible()方法;如果不是的话,实现onFragmentVisibleChange()方法;
最后,先解释一下BaseFragment中的几个设置的boolean值;
1.isFragmentVisible:fragment可见;true为可见;
2.isReuseView;View重用;
3.isFirstVisible;是第一次可见;
4.onFragmentVisibleChange(boolean isVisible)
有了这三个boolean 值和onFragmentVisibleChange(boolean isVisible) 方法的帮助,就可以实现对不同情况的梳理:
第一次可见,就是isFirstVisible为真时,调用onFragmentFirstVisible()进行数据加载;
从不可见到可见时,此时isVisibleToUser为真,进行不可见到可见的操作,调用onFragmentVisibleChange(true) 【传入其中的为true;】即可在使用的时候,实现从不可见到可见过程中的ui操作在这种情况下完成,只需要判断isVisible为真即可;并在此将isFragmentVisible的值设为true;
而isFragmentVisible是用来判断Fragment从可见到不可见的标志;因为setUserVisibleHint在这种情况下调用的时候使用这个来判断是最恰当的,因为此时,isVisibleToUser = false,且isVisibleToUser 不止在这一种情况下等于false,所以不能直接用isVisibleToUser = false来进行判断。在isFragmentVisible为真的情况下,将isFragmentVisible设为false,并调用onFragmentVisibleChange(false);在这个方法中实现从可见到不可见的ui操作;
使用方法:
使用很简单,新建你需要的 Fragment 类继承自该 BaseFragment,然后重写两个回调方法,根据你的需要在回调方法里进行相应的操作比如下载数据等即可。
例如:
public class CategoryFragment extends BaseFragment {
private static final String TAG = CategoryFragment.class.getSimpleName();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_category, container, false);
initView(view);
return view;
}
@Override
protected void onFragmentVisibleChange(boolean isVisible) {
if (isVisible) {
//更新界面数据,如果数据还在下载中,就显示加载框
//从不可见到可见
notifyDataSetChanged();
if (mRefreshState == STATE_REFRESHING) {
mRefreshListener.onRefreshing();
}
} else {
//关闭加载框
//从可见到不可见
mRefreshListener.onRefreshFinish();
}
}
@Override
protected void onFragmentFirstVisible() {
//去服务器下载数据
mRefreshState = STATE_REFRESHING;
mCategoryController.loadBaseData();
}
}
注意事项
1、如果想要让 fragment 的布局复用成功,需要重写 viewpager 的适配器里的 destroyItem() 方法,将 super 去掉,也就是不销毁 view。
2、如果出现切换回来或不相邻的Tab切换时导致空白界面的问题,解决方法:在 onCreateView中复用布局 + ViewPager 的适配器中复写 destroyItem() 方法去掉 super。
参考博客:
1.Android Fragment 生命周期onCreatView、onViewCreated - Sun的专栏 - CSDN博客 https://blog.csdn.net/asdf717/article/details/51383750
2.http://www.cnblogs.com/dasusu/p/6745032.html
再次感谢该博主的思路,谢谢。本篇后半段全是转载,若看正版,请点击上述链接(参考博客2),谢谢。