Fragment懒加载的探究和实现

前言
在Android开发中,利用ViewPager+Fragment实现页签的切换几乎是每个app的必备功能,虽然实现起来并不难,但是还是有一些需要我们注意的地方。我自己此前也了解过一些关于Fragment懒加载的实现,但是基本停留在拿来直接用的程度,很多地方还有一些疑惑,本文主要介绍一下使用ViewPager和Fragment的一些技巧和可能会踩到的坑,也算是对这块知识的梳理。

1.Fragment的懒加载

所谓懒加载,指的就是延迟加载,在需要的时候再加载数据,这个概念其实在Web开发中很常见,那么在Android开发中,为什么Fragment要实现懒加载?什么场景下需要实现懒加载呢?
相信我们在开发中都实现过底部和顶部的标签导航功能,点击相应的标签可以切换到相应的页面,当然使用的就是Fragment,由于同一时间只有一个页面(Fragment)能显示在屏幕中,因此没有显示出来的Fragment就没有必要在此时加载数据,特别是从网络获取数据这种比较耗时的操作,会产生不太好的用户体验,理想的情况是在Fragment可见的时候才加载数据,这就是为什么Fragment需要懒加载的原因。在实际开发中,实现Fragment的切换有两种方式,使用FragmentManagerViewPager,那么这两种情况下Fragment是何时加载的呢,我们来看两个常见场景:
场景一 使用FragmentManager实现底部导航栏页面切换
首先简单介绍一下FragmentManager,用于管理Fragment,能够实现Fragment的添加、移除、显示和隐藏,虽然我们可能已经用过很多次了,但还是有一些需要注意的地方。系统提供了三个API来获得FragmentManager,但是它们的使用场景是不一样的:

  • getSupportFragmentManager

getSupportFragmentManager()用于Activity中,用于管理Activity中添加的Fragment,该方法只有在FragmentActivity中才有,FragmentActivity是v4包中的,继承自Activity,用于兼容低版本没有Fragment的API问题,AppCompatActivity就是继承了FragmentActivity,因此如果我们的Activity是继承自AppCompatActivity,可以直接使用getSupportFragmentManager()方法来获得FragmentManager。

  • getFragmentManager

该方法既可用于Activity中,也可以用于Fragment中。该方法位于Activity类中,如果用于Activity中,与getSupportFragmentManager()方法作用相同,用于获取Activity中的FragmentManager,需要注意的是该方法是app包中的,因此如果我们使用的Fragment是v4包中的,那么应该让Activity继承自FragmentActivity,使用getSupportFragmentManager()。Fragment中也有该方法,返回的是管理当前Fragment自身的那个FragmentManager,也就是将当前Fragment添加进来的FragmentManager,有可能是Activity中的FragmentManager,也有可能是Fragment中的FragmentManager。

  • getChildFragmentManager

getChildFragmentManager()用于Fragment中,用于管理当前Fragment中添加的子Fragment,换句话说就是Fragment中嵌套Fragment的情况。

总结一下,在Activity中管理Fragment,如果Fragment是位于v4包中的,使用getSupportFragmentManager();如果Fragment是位于app包中的,使用getFragmentManager()。如果要在Fragment中嵌套子Fragment,使用getChildFragmentManager()。
实现底部导航栏的方式有很多:包括RadioButton、TabHost甚至是LinearLayout都可以,这里使用了官方design库提供的BottomNavigationView,使用方式不是本文的重点,也比较简单,这里就不提了,可以自行百度或是参考我的Demo。在使用时有几个需要注意的问题:

  • Tab标签多于3个时标签切换默认会有动画效果,就像这样:
    大于三个item切换动画

大多数情况下我们其实并不需要要这种效果,如何取消呢,针对design库的版本有不同的解决方法:
com.android.support:design:28.0.0以下:
通过反射调用setShiftingMode(false)方法,完整代码如下:

public void disableShiftMode(BottomNavigationView view) {
    BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0);
    try {
        Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
        shiftingMode.setAccessible(true);
        shiftingMode.setBoolean(menuView, false);
        shiftingMode.setAccessible(false);
        for (int i = 0; i < menuView.getChildCount(); i++) {
            BottomNavigationItemView item = (BottomNavigationItemView) menuView.getChildAt(i);
            //noinspection RestrictedApi
            item.setShiftingMode(false);
            // set once again checked value, so view will be updated
            //noinspection RestrictedApi
            item.setChecked(item.getItemData().isChecked());
        }
    } catch (NoSuchFieldException e) {
        Log.e("BNVHelper", "Unable to get shift mode field", e);
    } catch (IllegalAccessException e) {
        Log.e("BNVHelper", "Unable to change value of shift mode", e);
    }
}

使用时直接调用该方法,传入BottomNavigationView即可:

// BottomNavigationView禁止3个item以上动画切换效果
BottomNavigationViewHelper.disableShiftMode(mBottomNavigationView);

com.android.support:design:28.0.0:
无法调用setShiftingMode()方法,官方提供了解决方法,只需要在xml布局文件的BottomNavigationView下添加app:labelVisibilityMode="labeled"属性即可。

<android.support.design.widget.BottomNavigationView
    android:id="@+id/bnv_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:itemIconTint="@drawable/nav_item_color_state"
    app:itemTextColor="@drawable/nav_item_color_state"
    app:labelVisibilityMode="labeled"
    app:menu="@menu/menu_bottom_navigation" />
  • Tab切换时文字大小会变化
    其实这个效果是否需要保留因人而异,通过查看BottomNavigationItemView的源码我们可以发现选中和未选中字体的大小是由两个属性决定的。
public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) {
    ...
    int inactiveLabelSize =
            res.getDimensionPixelSize(android.support.design.R.dimen.design_bottom_navigation_text_size);
    int activeLabelSize = res.getDimensionPixelSize(
            android.support.design.R.dimen.design_bottom_navigation_active_text_size);
    ...
}

如果我们想要去掉切换时文字的大小变化,只要在自己项目中的values文件夹下新建dimens.xml文件,声明同名的属性,覆盖BottomNavigationView的默认属性值,将选中和未选中时的字体大小设置成相等的值就可以了。

<!-- BottomNavigationView选中和未选中文字大小 -->
<dimen name="design_bottom_navigation_active_text_size">14sp</dimen>
<dimen name="design_bottom_navigation_text_size">14sp</dimen>

下面回到正题,我给BottomNavigationView添加了三个标签,分别对应三个Fragment,每个Fragment的代码结构基本一致,只是加载的数据不一样,在Fragment的生命周期回调方法中打印日志,完整代码如下:

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.example.viewpagerfragment.R;
import com.example.viewpagerfragment.adapter.ListAdapter;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;

public class HomeFragment extends Fragment {

    private RecyclerView mRecyclerView;
    private ListAdapter mAdapter;
    private List<String> mData;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.e("TAG", "HomeFragment onAttach()");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e("TAG", "HomeFragment onCreate()");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.e("TAG", "HomeFragment onCreateView()");
        View view = inflater.inflate(R.layout.fragment_home, container, false);
        initView(view);
        initData();
        initEvent();
        return view;
    }

    /**
     * 初始化视图
     *
     * @param view
     */
    private void initView(View view) {
        mRecyclerView = view.findViewById(R.id.rv_home);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL));
    }

    /**
     * 初始化数据
     */
    private void initData() {
        mData = new ArrayList<>();
        // 模拟数据的延迟加载
        Observable.timer(3, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Exception {
                        for (int i = 0; i < 20; i++) {
                            mData.add("首页文章" + (i + 1));
                        }
                        mAdapter = new ListAdapter(getActivity(), mData);
                        mRecyclerView.setAdapter(mAdapter);
                    }
                });
    }

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

    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.e("TAG", "HomeFragment onActivityCreated()");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.e("TAG", "HomeFragment onStart()");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.e("TAG", "HomeFragment onResume()");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.e("TAG", "HomeFragment onPause()");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.e("TAG", "HomeFragment onStop()");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.e("TAG", "HomeFragment onDestroyView()");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e("TAG", "HomeFragment onDestroy()");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.e("TAG", "HomeFragment onDetach()");
    }
}

使用FragmentManager管理Fragment,调用hide()show()方法来切换页面的显示。

/**
 * 显示当前Fragment
 *
 * @param index
 */
private void showFragment(int index) {
    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    hideFragment(ft);
    switch (index) {
        case FRAGMENT_HOME:
            /**
             * 如果Fragment为空,就新建一个实例
             * 如果不为空,就将它从栈中显示出来
             */
            if (homefragment == null) {
                homefragment = new HomeFragment();
                ft.add(R.id.fl_container, homefragment, HomeFragment.class.getName());
            } else {
                ft.show(homefragment);
            }
            break;
        case FRAGMENT_KNOWLEDGESYSTEM:
            if (knowledgeSystemFragment == null) {
                knowledgeSystemFragment = new KnowledgeSystemFragment();
                ft.add(R.id.fl_container, knowledgeSystemFragment, KnowledgeSystemFragment.class.getName());
            } else {
                ft.show(knowledgeSystemFragment);
            }
            break;
        case FRAGMENT_PROJECT:
            if (projectFragment == null) {
                projectFragment = new ProjectFragment();
                ft.add(R.id.fl_container, projectFragment, ProjectFragment.class.getName());
            } else {
                ft.show(projectFragment);
            }
            break;
        default:
            break;
    }
    ft.commit();
}

/**
 * 隐藏全部Fragment
 *
 * @param ft
 */
private void hideFragment(FragmentTransaction ft) {
    // 如果不为空,就先隐藏起来
    if (homefragment != null) {
        ft.hide(homefragment);
    }
    if (knowledgeSystemFragment != null) {
        ft.hide(knowledgeSystemFragment);
    }
    if (projectFragment != null) {
        ft.hide(projectFragment);
    }
}

在Fragment的几个生命周期回调方法中打印日志,下面我们就来看一下整个加载过程。

  • 初始状态显示第一个Fragment

可以看出,此时依次回调了第一个Fragment的生命周期方法,并没有加载其他的两个Fragment。

  • 切换到第二个Fragment

此时依次回调了第二个Fragment的生命周期方法,并没有加载第三个Fragment,第一个Fragment也没有被销毁。

  • 切换到第三个Fragment

此时依次回调了第三个Fragment的生命周期方法,前两个Fragment并没有被销毁。之后在几个Fragment之间切换也不会回调任何的生命周期方法。
其实这种情况和我们理想的情况是一致的,即当Fragment第一次真正显示出来时才进行创建,加载数据,并且数据只加载一次。因此可以得出结论,当我们是通过调用hide()和show()方法来实现Fragment的切换时,不需要做额外的操作即可实现懒加载
那么可能有的人就会有疑问了,如果是调用replace()来实现Fragment的切换呢,会不会销毁掉之前的Fragment呢?下面来看一下这种情况。

/**
 * 显示当前Fragment
 *
 * @param index
 */
private void showFragment(int index) {
    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    switch (index) {
        case FRAGMENT_HOME:
            if (homefragment == null) {
                homefragment = new HomeFragment();
                ft.add(R.id.fl_container, homefragment, HomeFragment.class.getName());
            }
            ft.replace(R.id.fl_container, homefragment);
            break;
        case FRAGMENT_KNOWLEDGESYSTEM:
            if (knowledgeSystemFragment == null) {
                knowledgeSystemFragment = new KnowledgeSystemFragment();
                ft.add(R.id.fl_container, knowledgeSystemFragment, KnowledgeSystemFragment.class.getName());
            }
            ft.replace(R.id.fl_container, knowledgeSystemFragment);
            break;
        case FRAGMENT_PROJECT:
            if (projectFragment == null) {
                projectFragment = new ProjectFragment();
                ft.add(R.id.fl_container, projectFragment, ProjectFragment.class.getName());
            }
            ft.replace(R.id.fl_container, projectFragment);
            break;
        default:
            break;
    }
    ft.commit();
}
  • 初始状态显示第一个Fragment

与调用hide()和show()的情况没有区别,只是执行了第一个Fragment的生命周期方法。

  • 切换到第二个Fragment

这种情况下就有区别了,可以发现在调用第二个Fragment的生命周期方法同时销毁了第一个Fragment。

  • 切换到第三个Fragment

同上分析,创建第三个Fragment的同时回调了第二个Fragment销毁相关的生命周期方法。
之后切换回前几个Fragment,我们应该能够想到会发生什么情况,由于之前的Fragment已经被销毁,因此会重新创建Fragment,加载数据,同时销毁切换前的Fragment对象。
因此,当调用replace()实现Fragment的动态显示时,会销毁不可见的Fragment,重新创建当前Fragment,虽然Fragment也是在可见时加载数据的,但是会导致数据的多次加载,浪费资源,因此相比于hide()和show()方法,并不推荐这种方法切换Fragment。
最后总结一下,当我们使用FragmentManager管理多个Fragment,实现Fragment之间的切换时,有两种方法:hide()+show()或者replace(),两种方法的共同点是只有在Fragment显示时才创建Fragment对象,加载页面数据,也就是实现了懒加载,区别是前者在Fragment切换时不会销毁之前的Fragment对象,后者会销毁,推荐使用第一种方式,当然还是要看实际情况哪种方式更适合。
场景二 使用ViewPager实现顶部标签栏页面切换
实现顶部标签栏的方式同样有很多,github上很多优秀的第三方库,可以实现各种酷炫的效果,这里为了简单,依然是使用官方design库中提供的TabLayout,使用方式比较简单,就不展示了,配合ViewPager可以实现页面的滑动切换。
Fragment依然使用之前的那三个,完整代码如下:

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;

import com.example.viewpagerfragment.R;
import com.example.viewpagerfragment.adapter.MyPagerAdapter;

import java.util.ArrayList;
import java.util.List;

public class TabActivity extends AppCompatActivity {

    private TabLayout mTabLayout;
    private ViewPager mViewPager;

    private HomeFragment homefragment;
    private KnowledgeSystemFragment knowledgeSystemFragment;
    private ProjectFragment projectFragment;
    private List<Fragment> mFragments;
    private MyPagerAdapter mAdapter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab);
        initView();
        initData();
        initEvent();
    }

    /**
     * 初始化视图
     */
    private void initView() {
        mTabLayout = findViewById(R.id.tl_tabs);
        mViewPager = findViewById(R.id.vp_tabs);
    }

    /**
     * 初始化数据
     */
    private void initData() {
        mFragments = new ArrayList<>();
        homefragment = new HomeFragment();
        knowledgeSystemFragment = new KnowledgeSystemFragment();
        projectFragment = new ProjectFragment();
        mFragments.add(homefragment);
        mFragments.add(knowledgeSystemFragment);
        mFragments.add(projectFragment);

        mAdapter = new MyPagerAdapter(getSupportFragmentManager(), mFragments);
        mViewPager.setAdapter(mAdapter);
        // 关联ViewPager
        mTabLayout.setupWithViewPager(mViewPager);
        // mTabLayout.setupWithViewPager方法内部会remove所有的tabs,这里重新设置一遍tabs的text,否则tabs的text不显示
        mTabLayout.getTabAt(0).setText("首页");
        mTabLayout.getTabAt(1).setText("知识体系");
        mTabLayout.getTabAt(2).setText("项目");
    }

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

    }
}

这里提几点在使用TabLayout和ViewPager时需要注意的地方,调用setupWithViewPager()关联ViewPager后,TabLayout会remove掉所有的tab,运行后会发现无法显示标签文字。解决方法有两种:第一种是重写ViewPager的adapter的getPageTitle()方法,设置每个Tab的标题。

@Override
public CharSequence getPageTitle(int position) {
    String title;
    switch (position) {
        case 0:
            title = "首页";
            break;
        case 1:
            title = "知识体系";
            break;
        case 2:
            title = "项目";
            break;
        default:
            title = "";
            break;
    }
    return title;
}

第二种是在setupWithViewPager()后重新设置Tab标题,这种方式更适合与Tab个数和标题未知的情况。

mTabLayout.getTabAt(0).setText("首页");
mTabLayout.getTabAt(1).setText("知识体系");
mTabLayout.getTabAt(2).setText("项目");

ViewPager的Adapter有两种:FragmentPagerAdapterFragmentStatePagerAdapter,这两种的区别是什么呢,我们分别继承一下这两种Adapter,看一下效果。继承只需要实现几个方法就可以了,方法名一看就知道是什么意思,这里就不展示了。

  • 继承FragmentPagerAdapter

1.初始状态显示第一个Fragment

可以看出,此时不仅加载了第一个Fragment,第二个Fragment也创建并加载了。
2.切换到第二个Fragment

此时,第三个Fragment被创建并加载。
3.切换到第三个Fragment

此时,第一个Fragment依次执行onPause()、onStop()和onDestoryView()方法,注意只是销毁了视图,并没有执行onDestory()方法,销毁Fragment对象。
当我们重新切换到第二个Fragment时,第一个Fragment依次执行onCreateView()、onActivityCreate()、onStart()和onResume()方法,重新创建视图。

之后我们再切换回第一个Fragment,可以发现第三个Fragment的视图被销毁。

  • 继承FragmentStatePagerAdapter

下面我们再来看一下继承FragmentStatePagerAdapter的情况。
1.初始状态显示第一个Fragment

和继承FragmentPagerAdapter的情况没有区别,同样是创建加载出了前两个Fragment。
2切换到第二个Fragment

同样是提前创建出了第三个Fragment。
3.切换到第三个Fragment

这里就有区别了,同样是要销毁第一个Fragment,继承FragmentPagerAdapter时只是销毁了视图,并没有执行onDestory()方法;而继承FragmentStatePagerAdapter不仅会销毁视图,还销毁了Fragment对象,执行了onDestory()和onDetach()方法。
之后切换回第二个Fragment,会重新创建第一个Fragment对象,执行onAttach()和onCreate()方法。

再切换回第一个Fragment,第三个Fragment被销毁。

上面展示了不同情况下打印出来的生命周期执行日志,可能不是很清楚,这里我就总结一下,使用ViewPager切换Fragment时,默认会提前加载出下一个位置Fragment,与当前位置间隔超过1的Fragment会被销毁,这里又分为了两种情况:如果ViewPager的adapter继承自FragmentPagerAdapter,那么只会销毁Fragment的视图,不会销毁Fragment对象;如果ViewPager的adapter继承自FragmentStatePagerAdapter,那么不仅会销毁Fragment的视图,而且也会销毁Fragment对象(这好像是废话,对象都销毁了哪里来的视图)。
由于FragmentStatePagerAdapter会完全销毁Fragment对象,因此更适用于Fragment比较多的情况,保证Fragment的回收,节省内存;FragmentPagerAdapter更适合Fragment数量较少的情况,不会频繁地创建和销毁Fragment对象。
有人可能要问了,有没有什么方法可以防止Fragment的销毁呢?当然有了,这里先介绍一种方式,后面介绍ViewPager的预加载时会再介绍一种方法。通过查看FragmentStatePagerAdapter的源码会发现,Fragment的销毁是在destroyItem()方法中声明的(FragmentPagerAdapter也是这样),如果我们不需要销毁Fragment,只需要复写该方法即可,记住不要使用super调用父类的实现。

@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
//        super.destroyItem(container, position, object);
}

2.ViewPager的预加载机制

通过之前的例子我们已经知道ViewPager会提前加载下一个位置的Fragment,这就叫做VIewPager的预加载机制,作用是为了让ViewPager的切换更加流畅。提到ViewPager的预加载机制,我们就不得不提到一个方法setOffscreenPageLimit(int limit)。该方法的作用就是设置ViewPager的预加载页面数量,同时也决定了ViewPager能够缓存的页面数量。举个例子,如果我们调用mViewPager.setOffscreenPageLimit(3),那么ViewPager会提前加载当前页面两边相邻的3个Fragment,此时VIewPager可缓存的Fragment数量为2*3+1=7,与当前Fragment间距超过3的Fragment就会被销毁回收(是否会销毁Fragment实例对象由我们继承的Adapter决定)。limit的默认值是1,这就解释了为什么ViewPager会提前加载下一个位置的Fragment,并且显示第三个Fragment时会销毁第一个Fragment。
这里依然采用之前顶部标签栏的例子,添加一行代码,设置ViewPager的预加载数量,重新来看一下Fragment的创建和加载过程。

mViewPager.setOffscreenPageLimit(mFragments.size());
初始状态显示第一个Fragment

可以看出当初始状态显示第一个Fragment时,就已经创建并加载了所有的Fragment,并且当我们在几个Tab之间切换时,也不会销毁并重新创建Fragment。
这就是我在上文中提到的如何防止Fragment被销毁的第二种方法,就是通过setOffscreenPageLimit(),设置预加载数量为Tab总数,使得所有Fragment都能被缓存。
ViewPager的预加载机制其实和我们想要实现的懒加载是背道而驰的,那么我们可以取消预加载吗?答案是不能,或许有的人想到了设置预加载数量为0,但是并不起作用,这是为什么呢,我们来看一下setOffscreenPageLimit()方法内部就明白了。

private static final int DEFAULT_OFFSCREEN_PAGES = 1;

public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) {
        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                + DEFAULT_OFFSCREEN_PAGES);
        limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
}

我们可以很清楚地看出,如果我们传入了小于1的值,最后都会取默认值1,因此这种方法是无法取消预加载的。

3.如何实现Fragment的懒加载

既然我们无法取消ViewPager的预加载,那就只能从Fragment的角度来实现懒加载了。基本思路是判断Fragment是否可见,当可见时才加载数据,这就涉及到了Fragment的两个方法:setUserVisibleHint(boolean isVisibleToUser)onHiddenChanged(boolean hidden),下面我们就来具体看一下这两个方法。

  • setUserVisibleHint

setUserVisibleHint()方法只有在使用ViewPager管理Fragment时才会调用,有一个参数isVisibleToUser,字面意思就是是否对于用户可见,那么我们是否可以直接利用该参数来判断Fragment的可见与否呢?先别急,我们来看一下该方法的执行情况,依然采用之前顶部标签栏的例子,在每个Fragment中重写setUserVisibleHint()方法,打印isVisibleToUser的值。
首先来看一下初始状态显示第一个Fragment时的情况,由于已经设置了预加载数量为3,因此三个Fragment全部被创建和加载,但是我们注意setUserVisibleHint()方法的执行,对于第二和第三个Fragment来说,和我们预想中的一样,执行了一次,isVisibleToUser的值为false,也就是不可见;但对于第一个Fragment,setUserVisibleHint()方法执行了两次,并且第一次执行打印出来的isVisibleToUser的值为false,第二次才为true。再看一下setUserVisibleHint()的执行时机,我们发现该方法是在Fragment所有的生命周期方法之前就执行的,这一点需要注意。

再来看一下切换到第二个和第三个Fragment时的情况

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

这两种情况下就和预想的一样了,分别只执行了一次该方法,将相应Fragment的可见状态改变。
之后切换ViewPager都会执行两个Fragment的setUserVisibleHint()方法,不可见的的那个isVisibleToUser的值为false,显示出来的那个isVisibleToUser的值为true。

结合一开始显示第一Fragment时打印的结果来看,每个Fragment的setUserVisibleHint()方法都会至少执行两次,一次是在Fragment的生命周期方法执行之前,此时isVisibleToUser的值为false;一次是在Fragment变为可见时,此时isVisibleToUser的值为true。
这里还需要提一下getUserVisibleHint()方法,也有人是利用该方法来判断Fragment是否可见的,那么该方法的返回值代表什么呢,通过查看源码,我们可以发现,其实getUserVisibleHint()的返回值就是setUserVisibleHint()方法的isVisibleToUser参数,因此,这种判断方式本质上和利用isVisibleToUser来判断是一样的。

public void setUserVisibleHint(boolean isVisibleToUser) {
    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
            && mFragmentManager != null && isAdded()) {
        mFragmentManager.performPendingDeferredStart(this);
    }
    // 这里对mUserVisibleHint赋值
    mUserVisibleHint = isVisibleToUser;
    mDeferStart = mState < STARTED && !isVisibleToUser;
}

public boolean getUserVisibleHint() {
    return mUserVisibleHint;
}
  • onHiddenChanged

onHiddenChanged()方法只有在利用FragmentManager管理Fragment,并且使用hide()和show()方法切换Fragment时才会被调用,该方法同样有一个参数hidden,表示Fragment是否隐藏,下面我们就以之前底部导航栏的例子验证一下onHiddenChanged()方法的执行时机和作用。
首先是初始状态显示第一个Fragment时,可以发现并没有执行第一个Fragment的onHiddenChanged()方法,这是由于我在代码添加了判断,如果Fragment实例对象为空,就调用add()方法先将Fragment添加到FragmentTransaction中,并没有调用show()方法。

切换到第二个Fragment时,由于调用了hide()方法隐藏第一个Fragment,因此执行了第一个Fragment的onHiddenChanged()方法,hidden参数的值为true,表示隐藏了Fragment。

切换到第三个Fragment时同上,会调用第二个Fragment的onHiddenChanged()方法,hidden参数的值为true。大家可能注意到了,我在代码中是先调用了hide()方法隐藏了所有不为空的Fragment,那么为什么这里这里没有调用第一个Fragment的onHiddenChanged()方法呢,其实很简单,因为之前第一个Fragment就已经是隐藏状态了,我们注意方法名后缀是'Changed',因此只有在隐藏或显示状态改变的情况下才会调用onHiddenChanged()方法。

之后在任意两个Fragment之间切换时会分别执行两个Fragment的onHiddenChanged()方法,可见的那个hidden值为false,表示显示;不可见的hidden值为true,表示隐藏。

由此我们可以得出结论,onHiddenChanged()方法是在调用show()和hide()方法时被调用的,并且只有在Fragment的隐藏或显示状态发生了改变时才会调用。不同于setUserVisibleHint()方法,调用onHiddenChanged()时Fragment已经完成了创建相关生命周期(onAttach()~onResume())的回调。
既然已经清楚了这两个方法的调用时机和作用,那么我们就可以来实现懒加载了,首先确定实现思路:

  • 要在Fragment可见时加载数据,并且只加载一次。
  • 由于ViewPager的预加载机制,因此要利用setUserVisibleHint()方法,根据参数isVisibleToUser来判断Fragment是否可见。
  • setUserVisibleHint()方法不止会在Fragment切换时调用,在onCreateView()之前也会被调用,此时isVisibleToUser的值为false,这时是获取不到视图和控件的,因此不能只根据isVisibleToUser来判断是否需要加载数据,需要引入一个变量标识视图是否已经加载完成。
  • 由于加载数据后继续切换ViewPager仍然会执行setUserVisibleHint()方法,因此还需要引入一个变量标识是否已经加载过数据,防止数据的重复加载。
  • 只有当同时满足以下三个条件时才加载数据:
    • 视图已加载完成
    • 数据未加载
    • Fragment可见

清楚了思路后,我们就可以来封装自己的LazyFragment了,完整代码如下:

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public abstract class LazyFragment extends Fragment {

    private Context mContext;
    private boolean hasViewCreated; // 视图是否已加载
    private boolean isFirstLoad; // 是否首次加载

    private ProgressDialog mProgressDialog; // 加载进度对话框

    @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) {
        hasViewCreated = true;
        isFirstLoad = true;
        View view = LayoutInflater.from(mContext).inflate(getContentViewId(), null);
        initView(view);
        initData();
        initEvent();
        lazyLoad();
        return view;
    }

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

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

    }

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

    }

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

    }

    /**
     * 懒加载
     */
    protected void onLazyLoad(){

    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isVisibleToUser) {
            lazyLoad();
        }
    }

    private void lazyLoad() {
        if (!hasViewCreated || !isFirstLoad || !getUserVisibleHint()) {
            return;
        }
        isFirstLoad = false;
        onLazyLoad();
    }
}

像我前面分析的那样,声明了两个变量,分别标识视图是否加载完成和数据是否已加载,执行懒加载的条件有三个:视图已加载、数据未加载、isVisibleToUser的值为true。lazyLoad()方法的作用是判断是否可以加载数据,真正的加载数据逻辑在onLazyLoad()方法中声明。
关于上面的代码有几点我要说一下,第一点是为什么要在onCreateView中再执行一次lazyLoad()方法,我们前面分析过,setUserVisibleHint()是在onCreateView()之前调用的,这时hasViewCreated的值为false,不满足条件,是无法执行加载数据的逻辑的,因此要在onCreateView中将hasViewCreated设置为true之后再判断一次是否可以加载数据,这也是为什么我要单独写一个lazyLoad()方法的原因。第二点是我在lazyLoad()方法中使用了getUserVisibleHint()方法,之前提到过,该方法的返回值就是setUserVisibleHint()中的参数isVisibleToUser,因此可以直接利用该方法来判断Fragment的可见性,就不需要额外再声明一个变量了。使用方法也很简单,只需要继承LazyFragment,实现getContentViewId()方法,返回布局文件id即可。如果不需要实现懒加载,就重写initData()方法,内部添加数据加载逻辑;如果需要实现懒加载,就不需要重写initData()方法,将加载数据的逻辑放到onLazyLoad()方法中就可以了。
这样封装其实也有一个问题,就是当同一个Fragment同时需要用于FragmentManager场景和ViewPager场景中时,如果将加载数据逻辑放到onLazyLoad()中,那么在使用FragmentManager管理Fragment时不会调用setUsersetUserVisibleHint()方法,也就无法加载数据了;如果把加载数据逻辑放到initData()中,那么就失去了懒加载的作用。我有看到过一种解决方法是重写onHiddenChanged()方法,根据相同的判断条件,执行加载数据逻辑,但是这样有一个问题是在每一个Fragment第一次调用add()方法被添加后,需要手动调用hide()show()方法来触发onHiddenChanged()方法,个人觉得还是有些奇怪,这里就不展示了。考虑到这种情况也不是很常见,如果真的遇到了,还是写两个Fragment吧。
为了效果明显我在LazyFragment中添加了一个ProgressDialog来显示数据加载进度,我们来看一下实现懒加载后的效果,只有当ViewPager切换到Fragment时才开始加载数据,如下图所示:

Fragment实现懒加载

其实懒加载Fragment的具体封装方式有很多,但都是基于setUserVisibleHint()方法的,上面的代码只是我自己的一种封装,大家可以根据自己习惯的编码方式来实现自己的懒加载Fragment,重点还是要清楚原理和思路。

总结与后记

本文主要介绍了Fragment的懒加载实现以及ViewPager的预加载机制。实现Fragment的切换有两种方式:FragmentManager和ViewPager,其中前者不会提前加载Fragment,因此不需要实现懒加载;后者由于自身的预加载机制,需要考虑懒加载来使得页面的加载更加流畅。我们要清楚懒加载的实现并不是因为Fragment被延迟加载了,Fragment仍然会被预加载,只是当Fragment可见时才加载数据而已。
关于ViewPager和Fragment还有很多使用的技巧和可以深入去挖掘的东西,限于个人水平的原因,就不多提了,大家如果感兴趣可以查阅相关的资料。现在谷歌官方新推出了一个新的组件ViewPager2来取代ViewPager,支持了竖直方向的滑动,虽然由于兼容性等问题,短时间内ViewPager还不会被取代,但是有兴趣的话还是可以了解一下的。
本文的相关代码我已经上传到了github,由于自身水平的原因,我可能有些地方分析地不是很准确,表述地不是很清楚,欢迎大家指正,这样才能不断进步嘛。
Demo地址

参考文章

当Fragment遇上ViewPager

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

推荐阅读更多精彩内容