懒加载意思也就是当须要的时候才会去加载。android
通常咱们都会在 onCreate() 或者 onCreateView() 里去启动一些数据加载操做,好比从本地加载或者从服务器加载。缓存
大部分状况下,这样并不会出现什么问题,可是当你使用 ViewPager + Fragment的时候,问题就来了,这时就应该考虑是否须要实现懒加载了。服务器
ViewPager 为了让滑动的时候能够有很好的用户的体验,也就是防止出现卡顿现象,所以它有一个缓存机制。ide
默认状况下,ViewPager 会提早建立好当前 Fragment 旁的两个Fragment。函数
举个例子说, 也就是若是你当前显示的是编号 3 的Fragment,那么其实编号2和4的Fragment也已经建立好了,也就是说这3个Fragment都已经执行完 onAttach() -> onResume()
这之间的生命周期函数了。布局
原本 Fragment 的 onResume()表示的是当前 Fragment 处于可见且可交互状态,但因为ViewPager的缓存机制,它已经失去了意义,也就是说咱们只是打开了 “3” 这个Fragment,但其实“2” 和 “4”这两个Fragment的数据也都已经加载好了。ui
若是加载数据的操做都比较耗时或者都是相似图片的占用大量内存,这时就应该考虑想一想是否该实现懒加载。也就是, 当我打开哪一个Fragment的时候,它才会去加载数据 。code
setUserVisibleHint(boolean isVisibleToUser)
是否可行?不少人推荐说把加载数据的操做放在这个函数里,isVisibleToUser
表示当前 Fragment 是否可见。继承
若是你只是须要数据的懒加载的话,这个固然是能够的,但若是你还有如下的需求,那么这种方式就不行了:生命周期
这边再提一下,setUserVisibleHint()可能会在 Fragment 的生命周期以外被调用,也就是可能在view 建立前就被调用,也可能在 destroyView 后被调用,因此若是涉及到一些控件的操做的话,可能会报 null 异常,由于控件还没初始化,或者已经摧毁了。
自定义了一个新的回调函数 onFragmentVisibleChange(boolean isVisible)
,能够实现的效果有:
只有两种状况会触发该函数:
能够在该函数内进行控件的操做,不会报null异常
只有显示的那个 Fragment和 离开的那个 Fragment 才会触发回调函数,这样就能够支持咱们在可见状态变化时进行一些操做,由于不会有多余的false触发。
另外,由于ViewPager缓存机制,因此题主进行了 view 的复用,防止onCreateView()重复的建立view,其实也就是将view设置为成员变量,建立view时先判断是否为null。
由于ViewPager里对Fragment的回收和建立时,若是 Fragment已经建立过了,那么只会调用 onCreateView() -> onDestroyView() 生命函数,onCreate()和onDestroy并不会触发,因此关于变量的初始化和赋值操做能够在onCreate()里进行,这样就能够避免重复的操做。
具体代码以下:
/** * * Viewpager + Fragment状况下,fragment的生命周期因Viewpager的缓存机制而失去了具体意义 * 该抽象类自定义一个新的回调方法,当fragment可见状态改变时会触发的回调方法,介绍看下面 * * @see #onFragmentVisibleChange(boolean) */ public abstract class ViewPagerFragment extends Fragment { /** * rootView是否初始化标志,防止回调函数在rootView为空的时候触发 */ private boolean hasCreateView; /** * 当前Fragment是否处于可见状态标志,防止因ViewPager的缓存机制而致使回调函数的触发 */ private boolean isFragmentVisible; /** * onCreateView()里返回的view,修饰为protected,因此子类继承该类时,在onCreateView里必须对该变量进行初始化 */ protected View rootView; @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); Log.d(getTAG(), "setUserVisibleHint() -> isVisibleToUser: " + isVisibleToUser); if (rootView == null) { return; } hasCreateView = true; if (isVisibleToUser) { onFragmentVisibleChange(true); isFragmentVisible = true; return; } if (isFragmentVisible) { onFragmentVisibleChange(false); isFragmentVisible = false; } } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); initVariable(); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (!hasCreateView && getUserVisibleHint()) { onFragmentVisibleChange(true); isFragmentVisible = true; } } private void initVariable() { hasCreateView = false; isFragmentVisible = false; } /************************************************************** * 自定义的回调方法,子类可根据需求重写 *************************************************************/ /** * 当前fragment可见状态发生变化时会回调该方法 * 若是当前fragment是第一次加载,等待onCreateView后才会回调该方法,其它状况回调时机跟 {@link #setUserVisibleHint(boolean)}一致 * 在该回调方法中你能够作一些加载数据操做,甚至是控件的操做,由于配合fragment的view复用机制,你不用担忧在对控件操做中会报 null 异常 * * @param isVisible true 不可见 -> 可见 * false 可见 -> 不可见 */ protected void onFragmentVisibleChange(boolean isVisible) { Log.w(getTAG(), "onFragmentVisibleChange -> isVisible: " + isVisible); } }
用法 :
新建你须要的Fragment类,继承 ViewPagerFragment,在onCreateView()里对rootView进行初始化 -> 重写 onFragmentVisibleChange(),在这里进行你须要的操做,好比数据加载,控制显示等。
@Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { if (rootView == null) { rootView = inflater.inflate(R.layout.fragment_android, container, false); } return rootView; } @Override protected void onFragmentVisibleChange(boolean isVisible) { super.onFragmentVisibleChange(isVisible); if (isVisible) { // do things when fragment is visible // if (ListUtils.isEmpty(mDataList) && !isRefreshing()) { // setRefresh(true); // loadServiceData(false); } else { // setRefresh(false); } } }
有些时候,咱们打开一个 Fragment 页面时,但愿它是在可见时才去加载数据,也就是不要在后台就开始加载数据,并且,咱们也但愿加载数据的操做只是第一次打开该 Fragment 时才进行的操做,之后若是再从新打开该 Fragment 的话,就不要再重复的去加载数据了。
具体点说,Fragment 和 ViewPager 一块儿用时,因为 ViewPager 的缓存机制,在打开一个 Fragment 时,它旁边的几个Fragment 其实也已经被建立了,若是咱们是在 Fragment 的 onCreat() 或者 onCreateView() 里去跟服务器交互,下载界面数据,那么这时这些已经被建立的Fragment,就都会出如今后台下载数据的状况了。因此咱们一般须要在 setUserVisibleHint() 里去判断当前 Fragment 是否可见,可见时再去下载数据,可是这样仍是会出现一个问题,就是每次可见时都会重复去下载数据,咱们但愿的是只有第一次可见时才须要去下载,那么就还须要再作一些判断。这就是要封装个基类来作这些事了,具体代码见后面。
即便咱们在 setUserVisibleHint() 作了不少判断,实现了可见时加载而且只有第一次可见时才加载,可能仍是会遇到其余问题。
好比说,我下载完数据就直接须要对 ui 进行操做,将数据展现出来,但有时却报了 ui 控件 null 异常,这是由于 setUserVisibleHint() 有可能在 onCreateView() 建立 view 以前调用,并且数据加载时间很短,这就可能出现 null 异常了,那么咱们还须要再去作些判断,保证在数据下载完后 ui 控件已经建立完成。
除了懒加载,只加载一次的需求外,可能咱们还须要每次 Fragment 的打开或关闭时显示数据加载进度。对吧,咱们打开一个 Fragment 时,若是数据还没下载完,那么应该给个下载进度或者加载框提示,若是这个时候打开了新的 Fragment 页面,而后又从新返回时,若是数据还没加载完,那么也还应该继续给提示,对吧。这就须要有个 Fragment 可见与不可见时触发的回调方法,而且该方法还得保证是在 view 建立完后才触发的,这样才能支持对ui 进行操做。
/* * * Fragment基类,封装了懒加载的实现 * * 一、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; } }
使用很简单,新建你须要的 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(); } }
注意事项 :
若是想要让 fragment 的布局复用成功,须要重写 viewpager 的适配器里的 destroyItem() 方法,将 super 去掉,也就是不销毁 view。
若是出现切换回来或不相邻的 Tab 切换时致使空白界面的问题,解决方法:在 onCreateView中复用布局 + ViewPager 的适配器中复写 destroyItem() 方法去掉 super。