开发中,Fragment 最常见的两种使用方式就是 ViewPager 嵌套 Fragment ,以及直接通过FragmentManager 来管理 Fragment,对应的交互场景相信大家心里都有一个原型,没有的话也没关系,后边会有例子的。但这和懒加载有什么关系呢?试想一下,如果每个 Fragment 都有默认的网络请求操作(也可能是其它耗时操作,这里以网络请求为例),那么多个在 Fragment创建过程中都会执行默认网络请求,无论 Fragment 是否对用户可见,显然有些浪费流量、影响性 App 性能、用户体验不佳等缺点,这些自然不是我们想看到的,出于这些原因,让 Fragment 进行数据懒加载就有必要了。
先解释下为什么会出现多个 Fragment中的默认网络请求都会被执行,由于Fragment在创建的整个过程会走完从onAttach()
到onResume()
的生命周期方法,然而一般情况我们无非在这里几个生命周期方法(例如 onActivityCreated()
)里发起默认的网络请求,所以问题的原因显而易见,既然不能在 Fragment 生命周期方法直接请求数据,所以就要另谋它法。
我们要做的事情就是让 Fragment 按需加载数据,即对用户可见时再请求数据,让数据的请求时机可控,而不是在初始化创建过程中直接请求数据,同时不受嵌套层级的影响!
接下来我们结合文章开头提到的两种 Fragment 使用方式来实现 Fragment 懒加载的功能。
一、ViewPager 嵌套 Fragment
Fragment 有一个非生命周期的setUserVisibleHint(boolean isVisibleToUser)
回调方法,当 ViewPager 嵌套 Fragment 时会起作用,如果切换 ViewPager 则该方法也会被调用,参数isVisibleToUser
为true
代表当前 Fragment 对用户可见,否则不可见。
目测可以在这个方法中来判断是否请求数据,但在 Fragment 创建期间setUserVisibleHint()
方法是在onActivityCreated()
之前被调用,为了避免不必要的异常,所以在加载请求数据前需要做一个判断,就是相关的 UI 界面已经创建完毕、并且当前 Fragment 对用户可见,而且没请求过数据。先定义一个LazyLoadFragment
基类,继承自 BaseFragment,代码如下:
public abstract class LazyLoadFragment extends BaseFragment {
private boolean isViewCreated; // 界面是否已创建完成
private boolean isVisibleToUser; // 是否对用户可见
private boolean isDataLoaded; // 数据是否已请求
// 实现具体的数据请求逻辑
protected abstract void loadData();
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
this.isVisibleToUser = isVisibleToUser;
tryLoadData();
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewCreated = true;
tryLoadData();
}
public void tryLoadData() {
if (isViewCreated && isVisibleToUser && !isDataLoaded) {
loadData();
isDataLoaded = true;
}
}
}
ViewPager 中第一个可见的 Fragment 会走onActivityCreated()
方法去请求数据,之后切换 Fragment 会走setUserVisibleHint()
方法去尝试请求数据。这样我们的 Fragment 继承 LazyLoadFragment,然后实现loadData()
方法去完成数据的请求即可,写一个简单的 ViewPager 嵌套 Fragment 的界面,测试效果如下:
一切正常,符合我们的预期,由于现在只是一层嵌套,我们再加两层 ViewPager 嵌套 Fragment 试试,如下图(具体的实现可参考源码):
1、这里我们约定用 tab 标签上的编号指代对应的 Fragment,例如1-1代表最外层 ViewPager 的第一个 Fragment。
2、ViewPager 都设置setOffscreenPageLimit()
为其包含的 Fragment 个数
再次运行,只有1-1对用户可见,按照预期应该只有1-1请求了数据,但是2-1、3-1也请求了数据:
所以问题来了,虽然2-1、3-1对用户不可见,但在创建过程中它们的
setUserVisibleHint()
的isVisibleToUser
参数最终为true
,从而在onActivityCreated()
方法中请求了数据。注意此时2-1、3-1的父 Fragment 也是不可见的,所以要解决这个问题,可以在tryLoadData()
方法中判断当前要请求数据的 Fragment 的 父 Fragment 是否可见,不可见则不请求数据。
但新的问题又来了,这个导致该 Fragment 失去了初次请求数据的机会,即便该 Fragment 初次对用户可见时也不会主动去请求数据,需要来回再切换一次才会请求数据,要解决这个问题,可以让该 Fragment 的父 Fragment 请求数据时通知子 Fragment 去请求数据,修改下代码:
public abstract class LazyLoadFragment extends BaseFragment {
private boolean isViewCreated; // 界面是否已创建完成
private boolean isVisibleToUser; // 是否对用户可见
private boolean isDataLoaded; // 数据是否已请求, isNeedReload()返回false的时起作用
// 实现具体的数据请求逻辑
protected abstract void loadData();
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
this.isVisibleToUser = isVisibleToUser;
tryLoadData();
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewCreated = true;
tryLoadData();
}
/**
* ViewPager场景下,判断父fragment是否可见
*
* @return
*/
private boolean isParentVisible() {
Fragment fragment = getParentFragment();
return fragment == null || (fragment instanceof LazyLoadFragment && ((LazyLoadFragment) fragment).isVisibleToUser);
}
/**
* ViewPager场景下,当前fragment可见,如果其子fragment也可见,则尝试让子fragment请求数据
*/
private void dispatchParentVisibleState() {
FragmentManager fragmentManager = getChildFragmentManager();
List<Fragment> fragments = fragmentManager.getFragments();
if (fragments.isEmpty()) {
return;
}
for (Fragment child : fragments) {
if (child instanceof LazyLoadFragment && ((LazyLoadFragment) child).isVisibleToUser) {
((LazyLoadFragment) child).tryLoadData();
}
}
}
public void tryLoadData() {
if (isViewCreated && isVisibleToUser && isParentVisible() && !isDataLoaded) {
loadData();
isDataLoaded = true;
// 通知 子 Fragment 请求数据
dispatchParentVisibleState();
}
}
}
再次测试效果如下:
效果符合预期,由于1-2、2-1同时可见,所以会几乎同时请求数据,2-2、3-1也类似。
至此 ViewPager 嵌套 Fragment 形式的懒加载就实现了。
二、FragmentManager 管理 Fragment
FragmentManager 管理 Fragment 时,和 ViewPager 嵌套 Fragment 中的问题类似,但此时setUserVisibleHint()
方法并不会被调用,所以要寻找新的途径了。
当用 FragmentManager 来 add()
、hide()
、show()
Fragment 时 Fragment 的onHiddenChanged(boolean hidden)
方法会被调用,其中hidden
参数为false
时代表对应 Fragment 可见,否则不可见,注意三个操作里当执行 show()
操作时hidden
参数才为false
,同时由于该方法在onActivityCreated()
之后被调用。我们可以直接在onHiddenChanged()
方法参数为false
时发起数据请求即可。
当存在多层嵌套的情况时,即 FragmentManager 管理的 Fragment 内部又使用 FragmentManager 管理新的 Fragment,这种情况和多层 ViewPager 嵌套 Fragment 时的处理方法类似,即判断当前 Fragment 的父 Fragment 是否可见、以及 Fragment 可见时通知子 Fragment 去请求数据。
主要的问题就这些,看下代码实现:
public abstract class LazyLoadFragment extends BaseFragment {
private boolean isDataLoaded; // 数据是否已请求
private boolean isHidden = true; // 记录当前fragment的是否隐藏
// 实现具体的数据请求逻辑
protected abstract void loadData();
/**
* 使用show()、hide()控制fragment显示、隐藏时回调该方法
*
* @param hidden
*/
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
isHidden = hidden;
if (!hidden) {
tryLoadData1();
}
}
/**
* show()、hide()场景下,当前fragment没隐藏,如果其子fragment也没隐藏,则尝试让子fragment请求数据
*/
private void dispatchParentHiddenState() {
FragmentManager fragmentManager = getChildFragmentManager();
List<Fragment> fragments = fragmentManager.getFragments();
if (fragments.isEmpty()) {
return;
}
for (Fragment child : fragments) {
if (child instanceof LazyLoadFragment && !((LazyLoadFragment) child).isHidden) {
((LazyLoadFragment) child).tryLoadData1();
}
}
}
/**
* show()、hide()场景下,父fragment是否隐藏
*
* @return
*/
private boolean isParentHidden() {
Fragment fragment = getParentFragment();
if (fragment == null) {
return false;
} else if (fragment instanceof LazyLoadFragment && !((LazyLoadFragment) fragment).isHidden) {
return false;
}
return true;
}
/**
* show()、hide()场景下,尝试请求数据
*/
public void tryLoadData1() {
if (!isParentHidden() && !isDataLoaded) {
loadData();
isDataLoaded = true;
dispatchParentHiddenState();
}
}
}
实际的测试效果如下:
上边我们用isDataLoaded
控制 Fragment 只请求一次数据,如果需要每次 Fragment 可见都请求数据,我们只需对LazyLoadFragment
做如下修改:
public abstract class LazyLoadFragment extends BaseFragment {
/**
* fragment再次可见时,是否重新请求数据,默认为flase则只请求一次数据
*
* @return
*/
protected boolean isNeedReload() {
return false;
}
/**
* ViewPager场景下,尝试请求数据
*/
public void tryLoadData() {
if (isViewCreated && isVisibleToUser && isParentVisible() && (isNeedReload() || !isDataLoaded)) {
loadData();
isDataLoaded = true;
dispatchParentVisibleState();
}
}
/**
* show()、hide()场景下,尝试请求数据
*/
public void tryLoadData1() {
if (!isParentHidden() && (isNeedReload() || !isDataLoaded)) {
loadData();
isDataLoaded = true;
dispatchParentHiddenState();
}
}
}
添加了一个isNeedReload()
方法,如果子类需要每次可见都请求数据,重写该方法返回true
即可。
1、对于Activity,
getSupportFragmentManager()
得到的是FragmentActivity的FragmentManager对象
2、对于Fragment,getFragmentManager()
得到的是父Fragment的FragmentManager对象,如果没有父Fragment,则是FragmentActivity的FragmentManager对象
3、getChildFragmentManager()
得到是Fragment自身的FragmentManager对象
至此,LazyLoadFragment
基类就完成了,只要继承它就可以轻松实现懒加载功能,更多细节戳这里:LazyLoadFragment