从了解到弄清,解决Fragment的懒加载

写在前面

Fragment的懒加载,在我看来就是为了抵抗ViewPager的预加载机制所做的抵抗。这样会使得用户不需要在一开始就加载好之后的内容,可以节约资源,也可以避免网络堵塞,增强用户体验。
一、名词解释

ViewPager为了让滑动的时候可以有很好的用户的体验,也就是防止出现卡顿现象,因此它有一个缓存机制。默认情况下,ViewPager会提前创建好当前Fragment旁的两个Fragment,举个例子说也就是如果你当前显示的是编号3的Fragment,那么其实编号2和4的Fragment也已经创建好了,也就是说这3个Fragment都已经执行完 onAttach() -> onResume() 这之间的生命周期函数了。
什么是懒加载,就是只用到了该用它的时候它才加载。只有当Fragment被切换到了当前页面的时候,才让它去请求数据。

二、实现具体思路
在这个懒加载需求的面前,我们很容易就想到,如果有一个方法,他可以做到在Fragment呈现到我们面前的时候才会去加载数据的话,那么就可以直接完成这个需求。在我不懈的百度之下,我发现了下面这个方法。

setUserVisibleHint(boolean isVisibleToUser)

传闻说,只要将加载数据的操作放到这个里面就可以实现我们的需求,我们来试试。结果如下图所示:

setUserVisibleHint.png

这里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),谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容