Android学习小计:androidx中的Fragment懒加载方案


在进入正文之前要强调一下,本文的分析基于androidx 1.1.0版本,文中提到的setMaxLifecycle()方法是1.1.0-alpha07版本才引入的。
最近把Android Studio更新到了3.5版本,新建项目时发现竟然已经强制使用androidx包了。

于是想着把以前项目中的一些公共类,像BaseActivity、BaseFragment等等都迁移到androidx方便今后的开发,要做的也很简单,就是重新导包,将原来的“support系列”替换为“androidx系列”。简单的迁移完成后我发现此前实现懒加载Fragment中重写的setUserVisibleHint()方法已经过时了。关于setUserVisibleHint()方法我此前写过一篇Fragment懒加载的探究和实现进行了详细介绍,网上也有很多相关的资料,我这里就不说了。

setUserVisibleHint()方法已过时

我们点进setUserVisibleHint()方法的源码来看一下,对于过时的方法源码中都会注明使用哪个方法来替代:

/**
 * Set a hint to the system about whether this fragment's UI is currently visible
 * to the user. This hint defaults to true and is persistent across fragment instance
 * state save and restore.
 *
 * <p>An app may set this to false to indicate that the fragment's UI is
 * scrolled out of visibility or is otherwise not directly visible to the user.
 * This may be used by the system to prioritize operations such as fragment lifecycle updates
 * or loader ordering behavior.</p>
 *
 * <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
 * and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
 *
 * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
 *                        false if it is not.
 *
 * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)}
 * instead.
 */
@Deprecated
public void setUserVisibleHint(boolean isVisibleToUser) {
    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
            && mFragmentManager != null && isAdded() && mIsCreated) {
        mFragmentManager.performPendingDeferredStart(this);
    }
    mUserVisibleHint = isVisibleToUser;
    mDeferStart = mState < STARTED && !isVisibleToUser;
    if (mSavedFragmentState != null) {
        // Ensure that if the user visible hint is set before the Fragment has
        // restored its state that we don't lose the new value
        mSavedUserVisibleHint = isVisibleToUser;
    }
}

可以看到注释中写明了使用FragmentTransaction的setMaxLifecycle()方法来替代setUserVisibleHint()方法,这个setMaxLifecycle()方法是什么呢,下面我们就来具体看一下。

setMaxLifecycle()方法

/**
 * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is
 * already above the received state, it will be forced down to the correct state.
 *
 * <p>The fragment provided must currently be added to the FragmentManager to have it's
 * Lifecycle state capped, or previously added as part of this transaction. The
 * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise
 * an {@link IllegalArgumentException} will be thrown.</p>
 *
 * @param fragment the fragment to have it's state capped.
 * @param state the ceiling state for the fragment.
 * @return the same FragmentTransaction instance
 */
@NonNull
public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
                                           @NonNull Lifecycle.State state) {
    addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
    return this;
}

setMaxLifecycle()方法定义在FragmentTransaction类中,它的内部逻辑很简单,其实我们经常使用的add()remove()show()hide()等方法也是类似的逻辑,将操作封装为一个Op对象,最后调用commit()方法时再根据Op对象执行对应的操作。
注释中提到setMaxLifecycle()方法的作用是为Fragment的状态设置上限,如果当前Fragment的状态已经超过了设置的上限,就会强制被降到相应状态。在弄清楚上面这段文字的意义之前我首先要介绍两个相关概念:Fragment的状态Lifecycle的状态

  • Fragment的状态

在Fragment类中定义了5个int常量,表示Fragment的状态值:

static final int INITIALIZING = 0;     // Not yet created.
static final int CREATED = 1;          // Created.
static final int ACTIVITY_CREATED = 2; // Fully created, not started.
static final int STARTED = 3;          // Created and started, not resumed.
static final int RESUMED = 4;          // Created started and resumed.
  • Lifecycle的状态

Lifecycle是Android Jetpack中的架构组件之一,用于帮助我们方便地管理Activity和Fragment的生命周期,关于Lifecycle的详细介绍和使用网上有很多文章,我这里就不说了,如果此前没有接触过可以自行了解一下哈。
在Lifecycle定义了一个枚举类State

public enum State {
    DESTROYED,
    INITIALIZED,
    CREATED,
    STARTED,
    RESUMED;
    public boolean isAtLeast(@NonNull State state) {
        return compareTo(state) >= 0;
    }
}

可以看出Lifecycle中同样定义了5个状态,不过这里的状态和Fragment中定义的状态还是有一些区别的。
回到setMaxLifecycle()方法,需要传入的参数有两个:fragment和state。fragment不用多说,就是要设置的目标Fragment,不过需要注意的是此时Fragment必须已经被添加到了FragmentManager中,也就是调用了add()方法,否则会抛出异常。state就是Lifecycle中定义的枚举类型,同样需要注意传入的state应该至少为CREATED,换句话说就是只能传入CREATED、STARTED和RESUMED,否则同样会抛出异常。
看到这里可能会产生一个疑问,传入的state是Lifecycle中定义的,怎样转换为对Fragment状态的限制呢,上面也看到了,这两个状态还是有区别的。而且状态这个概念平时可能接触得不多,下面就以我们最熟悉的生命周期方法来说明这个状态的限制,先上一张图总结一下结论:

Fragment状态与Lifecycle.State以及生命周期方法的关系

图中展示了Fragment状态间切换会执行的生命周期方法以及Lifecycle.State对应的Fragment状态,由于setMaxLifecycle()方法要求传入的state至少为CREATED,因此我们只需研究CREATEDSTARTEDRESUMED这三个状态,结合上图解释一下setMaxLifecycle()方法的作用。

  • 参数传入Lifecycle.State.CREATED

Lifecycle.State.CREATED对应Fragment的CREATED状态,如果当前Fragment状态低于CREATED,也就是INITIALIZING,那么Fragment的状态会变为CREATED,依次执行onAttach()onCreate()方法;如果当前Fragment状态高于CREATED,那么Fragment的状态会被强制降为CREATED,以当前Fragment状态为RESUMED为例,接下来会依次执行onPause()onStop()onDestroyView()方法。如果当前Fragment的状态恰好为CREATED,那么就什么都不做。

  • 参数传入Lifecycle.State.STARTED

Lifecycle.State.STARTED对应Fragment的STARTED状态,如果当前Fragment状态低于STARTED,那么Fragment的状态会变为STARTED,以当前Fragment状态为CREATED为例,接下来会依次执行onCreateView()onActivityCreate()onStart()方法;如果当前Fragment状态高于STARTED,也就是RESUMED,那么Fragment的状态会被强制降为STARTED,接下来会执行onPause()方法。如果当前Fragment的状态恰好为STARTED,那么就什么都不做。

  • 参数传入Lifecycle.State.RESUMED

Lifecycle.State.RESUMED对应Fragment的RESUMED状态,如果当前Fragment状态低于RESUMED,那么Fragment的状态会变为RESUMED,以当前Fragment状态为STARTED为例,接下来会执行onResume()方法。如果当前Fragment的状态恰好为RESUMED,那么就什么都不做。
光介绍结论可能不是很直观,下面就以几个常见的例子来验证一下上图的结论。

  • 传入Lifecycle.State.CREATED
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
homefragment = new HomeFragment();
ft.add(R.id.fl_container, homefragment);
ft.setMaxLifecycle(homefragment, Lifecycle.State.CREATED);
ft.commit();

Fragment生命周期方法执行如下:

可以看出,在调用setMaxLifecycle()方法传入了Lifecycle.State.CREATED后,会限制Fragment的状态为CREATED,因此只会执行到onCreate()方法,而不会执行后面的onCreateView()-onResume()方法。
再来看“降级”的例子,调用setMaxLifecycle()之前已经调用过add()方法将Fragment添加到了FragmentManager中,此时Fragment的状态为RESUMED

FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setMaxLifecycle(homefragment, Lifecycle.State.CREATED);
ft.commit();

Fragment生命周期方法执行如下:

可以看出此时Fragment的状态会降为CREATED,依次执行onPause()onStop()onDestroyView()方法。

  • 传入Lifecycle.State.STARTED
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
homefragment = new HomeFragment();
ft.add(R.id.fl_container, homefragment);
ft.setMaxLifecycle(homefragment, Lifecycle.State.STARTED);
ft.commit();

Fragment生命周期方法执行如下:

可以看出,在调用setMaxLifecycle()方法传入了Lifecycle.State.STARTED后,会限制Fragment的状态为STARTED,因此只会执行到onStart()方法,而不会执行后面的onResume()方法。
同样看一下“降级”的例子,调用setMaxLifecycle()之前已经调用过add()方法将Fragment添加到了FragmentManager中,此时Fragment的状态为RESUMED

FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setMaxLifecycle(homefragment, Lifecycle.State.STARTED);
ft.commit();

Fragment生命周期方法执行如下:

可以看出此时Fragment的状态会降为STARTED,执行onPause()方法。

  • 传入Lifecycle.State.RESUMED

在上一个例子的基础上执行下面的代码:

FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setMaxLifecycle(homefragment, Lifecycle.State.RESUMED);
ft.commit();

Fragment生命周期方法执行如下:

可以看出,在调用setMaxLifecycle()方法传入了Lifecycle.State.RESUMED后,会限制Fragment的状态为RESUMED,由于当前Fragment的状态为STARTED,接下来会执行onResume()方法。

懒加载的新方案

此前Fragment的懒加载都是通过setUserVisibleHint()方法来实现,既然官方提出了setMaxLifecycle()方法来替代setUserVisibleHint()方法,那么肯定也提供了懒加载的新方案,这次的切入点在FragmentPagerAdapter中,我们会发现之前继承自FragmentPagerAdapter的构造方法同样过时了。

同样地,我们点进这个构造方法看一下:

/**
 * Constructor for {@link FragmentPagerAdapter} that sets the fragment manager for the adapter.
 * This is the equivalent of calling {@link #FragmentPagerAdapter(FragmentManager, int)} and
 * passing in {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}.
 *
 * <p>Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the
 * current Fragment changes.</p>
 *
 * @param fm fragment manager that will interact with this adapter
 * @deprecated use {@link #FragmentPagerAdapter(FragmentManager, int)} with
 * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}
 */
@Deprecated
public FragmentPagerAdapter(@NonNull FragmentManager fm) {
    this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
}

可以看到方法内部又调用了两个参数的构造方法,注释中也写明了要使用两个参数的构造方法,我们来看一下这个新的构造方法:

public FragmentPagerAdapter(@NonNull FragmentManager fm,
                            @Behavior int behavior) {
    mFragmentManager = fm;
    mBehavior = behavior;
}

多了一个int类型的参数behavior,可选的值有以下两个:

@Deprecated
public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;

public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;

可以看出一个参数的构造方法默认传入BEHAVIOR_SET_USER_VISIBLE_HINT,将其赋值给mBehavior,那么这个mBehavior在什么地方用到了呢。在FragmentPagerAdapter.java文件中全局搜索一下,发现只有两个地方用到了mBehavior:instantiateItem()方法和setPrimaryItem()方法。instantiateItem()方法我们很熟悉,是初始化ViewPager中每个Item的方法,setPrimaryItem()方法我此前没有接触过,简单地看了一下源码发现它的作用是设置ViewPager将要显示的Item,在ViewPager切换时会调用该方法,我们来看一下FragmentPagerAdapter中的setPrimaryItem()方法:

@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment) object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                if (mCurTransaction == null) {
                    mCurTransaction = mFragmentManager.beginTransaction();
                }
                // 如果mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,则将上一个Fragment的状态设置为STARTED
                mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
            } else {
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
        }
        fragment.setMenuVisibility(true);
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            if (mCurTransaction == null) {
                mCurTransaction = mFragmentManager.beginTransaction();
            }
            // 如果mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,则将当前Fragment的状态设置为RESUMED
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
        } else {
            fragment.setUserVisibleHint(true);
        }
        
        mCurrentPrimaryItem = fragment;
    }
}

方法的逻辑还是很简单的,如果mBehavior的值为BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,那么就调用setMaxLifecycle()方法将上一个Fragment的状态设置为STARTED,将当前要显示的Fragment的状态设置为RESUMED;反之如果mBehavior的值为BEHAVIOR_SET_USER_VISIBLE_HINT,那么依然使用setUserVisibleHint()方法设置Fragment的可见性,相应地可以根据getUserVisibleHint()方法获取到Fragment是否可见,从而实现懒加载,具体做法我就不说了。
下面我们来具体看一下mBehavior为BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT的情况,首先在ViewPager的Adapter中调用两个参数的构造方法:

public MyPagerAdapter(@NonNull FragmentManager fm, int behavior, List<Fragment> fragments) {
    super(fm, behavior);
    this.fragments = fragments;
}

然后在构造Adapter对象的时候behavior参数传入FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT

mAdapter = new MyPagerAdapter(getSupportFragmentManager(),FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,mFragments);

其他的代码就不展示了,这里我给ViewPager添加了三个Fragment,并调用了setOffscreenPageLimit(3)来预加载所有的Fragment,下面我们就来看一下几种情况下三个Fragment生命周期方法的执行情况。

  • 默认情况下显示第一个Fragment

可以看出第一个Fragment此时可见,执行到了onResume()方法,即RESUMED状态,第二个和第三个Fragment此时不可见,均执行到onStart()方法,即STARTED状态。

  • 从第一个Fragment切换到第二个Fragment

可以看出第一个Fragment此时变为不可见,由于调用setMaxLifecycle()方法传入了Lifecycle.State.STARTED,因此会从RESUMED状态变为STARTED状态,执行onPause()方法;第二个Fragment由不可见变为可见,由于调用setMaxLifecycle()方法传入了Lifecycle.State.RESUMED,因此会从STARTED状态变为RESUMED状态,执行onResume()方法。

  • 从第二个Fragment切换到第三个Fragment

和上一个情况相同,第二个Fragment变为不可见,执行onPause()方法,状态变为STARTED;第三个Fragment变为可见,执行onResume()方法,状态变为RESUMED
之后在几个Fragment之间切换的情况也是一样的,变为不可见的Fragment执行onPause()方法,变为可见的Fragment执行onResume()方法。
从以上几种情况中我们不难发现,在Fragment变为可见时都会执行onResume()方法,我们可以利用这一点来实现懒加载,基本思路有两点:

  • 将Fragment加载数据的逻辑放到onResume()方法中,这样就保证了Fragment可见时才会加载数据。
  • 声明一个变量标记是否是首次执行onResume()方法,因为每次Fragment由不可见变为可见都会执行onResume()方法,需要防止数据的重复加载。此外,如果我们使用的是FragmentPagerAdapter,切换导致Fragment被销毁时是不会执行onDestory()onDetach()方法的,只会执行到onDestroyView()方法,因此在onDestroyView()方法中我们还需要将这个变量重置,否则当Fragment再次可见时就不会重新加载数据了。

按照以上两点我们就可以封装出新的懒加载Fragment了,完整代码如下:

public abstract class NewLazyFragment extends Fragment {

    private Context mContext;
    private boolean isFirstLoad = true; // 是否第一次加载

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = getActivity();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = LayoutInflater.from(mContext).inflate(getContentViewId(), null);
        initView(view);
        return view;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        isFirstLoad = true;
    }

    @Override
    public void onResume() {
        super.onResume();
        if (isFirstLoad) {
            // 将数据加载逻辑放到onResume()方法中
            initData();
            initEvent();
            isFirstLoad = false;
        }
    }

    /**
     * 设置布局资源id
     *
     * @return
     */
    protected abstract int getContentViewId();

    /**
     * 初始化视图
     *
     * @param view
     */
    protected void initView(View view) {

    }

    /**
     * 初始化数据
     */
    protected void initData() {

    }

    /**
     * 初始化事件
     */
    protected void initEvent() {

    }
}

总结

本文主要介绍了androidx包中setMaxLifecycle()方法的使用和作用,它用于替代setUserVisibleHint()方法,可以设置Fragment的状态,由此引出了实现Fragment懒加载的新方案,在构造FragmentPagerAdapter时传入BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,将加载数据的逻辑放到Fragment的onResume()方法中即可。
最后还是要感叹一下,此前我一直忌惮androidx迁移的坑,这次由于Android Studio的限制我才第一次真正使用androidx,虽然在迁移的过程中难免会有一些问题,但这毕竟是早晚要解决的,而且androidx不仅仅是统一了support包,还引入了一些新的内容,为开发者提供了很多便利,今后还是有必要好好学习一下的。
本文的相关代码我已经上传到了github,可以参考一下,此外如果我有什么分析得不对的地方欢迎大家提出指正。
Demo地址

参考文章

Fragment新功能,setMaxLifecycle了解一下

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

推荐阅读更多精彩内容