【Fragment】懒加载实现

故事扮演 提交于 2020-01-07 05:08:41

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

一、何为懒加载

懒加载意思也就是当需要的时候才会去加载。

1.1、 为什么Fragment需要懒加载呢?

一般我们都会在 onCreate() 或者 onCreateView() 里去启动一些数据加载操作,比如从本地加载或者从服务器加载。

大部分情况下,这样并不会出现什么问题,但是当你使用 ViewPager + Fragment的时候,问题就来了,这时就应该考虑是否需要实现懒加载了。

1.2、 viewpager + fragment 坑

ViewPager 为了让滑动的时候可以有很好的用户的体验,也就是防止出现卡顿现象,因此它有一个缓存机制。

默认情况下,ViewPager 会提前创建好当前 Fragment 旁的两个Fragment。

举个例子说, 也就是如果你当前显示的是编号 3 的Fragment,那么其实编号2和4的Fragment也已经创建好了,也就是说这3个Fragment都已经执行完 onAttach() -> onResume() 这之间的生命周期函数了。

本来 Fragment 的 onResume()表示的是当前 Fragment 处于可见且可交互状态,但由于ViewPager的缓存机制,它已经失去了意义,也就是说我们只是打开了 “3” 这个Fragment,但其实“2” 和 “4”这两个Fragment的数据也都已经加载好了。

如果加载数据的操作都比较耗时或者都是类似图片的占用大量内存,这时就应该考虑想想是否该实现懒加载。也就是, 当我打开哪个Fragment的时候,它才会去加载数据

二、懒加载的实现

2.1、 setUserVisibleHint(boolean isVisibleToUser) 是否可行?

很多人推荐说把加载数据的操作放在这个函数里,isVisibleToUser 表示当前 Fragment 是否可见。

如果你只是需要数据的懒加载的话,这个当然是可以的,但如果你还有以下的需求,那么这种方式就不行了:

  • 如果你在Fragment可见时需要进行一些控件的操作,比如显示加载控件
  • 如果你还需要在Fragment从 “可见 -> 不可见” 时进行一些操作的话,比如取消加载控件显示

这边再提一下,setUserVisibleHint()可能会在 Fragment 的生命周期之外被调用,也就是可能在view 创建前就被调用,也可能在 destroyView 后被调用,所以如果涉及到一些控件的操作的话,可能会报 null 异常,因为控件还没初始化,或者已经摧毁了。

2.2、 进一步改进

自定义了一个新的回调函数 onFragmentVisibleChange(boolean isVisible),可以实现的效果有:

  • 只有两种情况会触发该函数:

    • 一种是Fragment从“不可见 -> 可见” 时触发,并传 入 isVisible = true;
    • 一种是Fragment从“可见 -> 不可见” 时触发,并传入 isVisible = false
  • 可以在该函数内进行控件的操作,不会报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);
            }
        }
    }


三、只加载一次

3.1、支持数据的懒加载并且只加载一次

有些时候,我们打开一个 Fragment 页面时,希望它是在可见时才去加载数据,也就是不要在后台就开始加载数据,而且,我们也希望加载数据的操作只是第一次打开该 Fragment 时才进行的操作,以后如果再重新打开该 Fragment 的话,就不要再重复的去加载数据了。

具体点说,Fragment 和 ViewPager 一起用时,由于 ViewPager 的缓存机制,在打开一个 Fragment 时,它旁边的几个Fragment 其实也已经被创建了,如果我们是在 Fragment 的 onCreat() 或者 onCreateView() 里去跟服务器交互,下载界面数据,那么这时这些已经被创建的Fragment,就都会出现在后台下载数据的情况了。所以我们通常需要在 setUserVisibleHint() 里去判断当前 Fragment 是否可见,可见时再去下载数据,但是这样还是会出现一个问题,就是每次可见时都会重复去下载数据,我们希望的是只有第一次可见时才需要去下载,那么就还需要再做一些判断。这就是要封装个基类来做这些事了,具体代码见后面。

3.2、提供 Fragment 可见与不可见时回调,支持你在这里进行一些 ui 操作,如显示/隐藏加载框

即使我们在 setUserVisibleHint() 做了很多判断,实现了可见时加载并且只有第一次可见时才加载,可能还是会遇到其他问题。

比如说,我下载完数据就直接需要对 ui 进行操作,将数据展示出来,但有时却报了 ui 控件 null 异常,这是因为 setUserVisibleHint() 有可能在 onCreateView() 创建 view 之前调用,而且数据加载时间很短,这就可能出现 null 异常了,那么我们还需要再去做些判断,保证在数据下载完后 ui 控件已经创建完成。

除了懒加载,只加载一次的需求外,可能我们还需要每次 Fragment 的打开或关闭时显示数据加载进度。对吧,我们打开一个 Fragment 时,如果数据还没下载完,那么应该给个下载进度或者加载框提示,如果这个时候打开了新的 Fragment 页面,然后又重新返回时,如果数据还没加载完,那么也还应该继续给提示,对吧。这就需要有个 Fragment 可见与不可见时触发的回调方法,并且该方法还得保证是在 view 创建完后才触发的,这样才能支持对ui 进行操作。

3.3、支持 view 的复用,防止与 ViewPager 使用时出现重复创建 view 的问题

3.4、示例代码

/*
 *
 * 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;
    }
}

使用很简单,新建你需要的 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。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!