ViewPager和Fragment一篇就夠了

ViewPager顯示多Fragment使用問題

前言:每當使用ViewPager時,對于選用什么適配器,緩存多少頁面,是否需要懶加載以及Fragment的數據刷新經常會有些疑問,網絡上的答案很多,但是很少有一篇能夠對一些疑問進行總結,本文主要在于記錄,方便日后查看。

1.FragmentPagerAdapter和FragmentPagerStateAdapter的區別,使用場景

setOffScreenPageLimit(int limit)設置viewpager左右預加載頁

區別:

FragmentPagerAdapter將每一個生成的Fragment保存在內存中,limit外Fragment沒有銷毀,生命周期為onPause->onStop->onDestroyView,onCreateView->onStart->onResume,但Fragment的成員變量都沒有變,所以可以緩存根View,避免重復inflate。

FragmentPageAdater下Fragment的生命周期.png
private View mRootView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    Log.e(TAG, "onCreateView: page_" + mPosition);
    if (mRootView == null) {
        mRootView = inflater.inflate(R.layout.fragment_test, container, false);
        initView(mRootView);
    }
    return mRootView;
}

FragmentStatePagerAdapter對limit外的Fragment銷毀,生命周期為onPause->onStop->onDestoryView->onDestory->onDetach, onAttach->onCreate->onCreateView->onStart->onResume。

FragmentStatePageAdapter下Fragment的生命周期.png

使用場景:對于需要緩存在內存中的固定較少數量的靜態頁面使用FragmentPagerAdapter,如引導頁,Tab頁面;對于擁有大量頁面的情況應使用FragmentStatePagerAdapter避免占用大量內存,如圖片預覽

2.是否有必要在適配器的public Fragment getItem(int position)方法中返回緩存List<Fragment>中的Fragment

對于FragmentPagerAdapter,instantiateItem()先從FragmentManager.findFragmentByTag()中查找FragmentManager中List緩存的Fragment,取不到則會調用getItem(),所以對于緩存在內存中的FragmentPagerAdapter沒有必要再使用一個List緩存Fragment,因為FragmentPagerAdapter會緩存每一個加載過的Fragment到內存中。

instantiateItem.png
makeFragmentName.png

對于FragmentStatePagerAdapter的instantiateItem()則會緩存limit左右的Fragment,超過limit則會回收,當Fragment沒有緩存時重新調用getItem(),因為頁面比較多,所以也沒必要使用List緩存Fragment占用內存,否則FragmentStatePagerAdapter沒有意義。

instantiateItem.png
fragments.png

3.ViewPager為什么要懶加載,什么情況適用?

ViewPager的setOffScreenPageLimit()方法默認limit為1,既會預加載左右頁面,而為了節省流量,理想情況是當用戶切換到該界面時才會調用網絡請求獲取數據。相關方法為setUserVisibleHint(),當前頁面為true,預加載頁面為false,只有Fragment從可見到不可見或者從不可見到可見時會調用,Fragment初次創建時setUserVisibleHint先于onCreateView()調用,所以可以由此判斷Fragment是否初始創建。

ViewPager首次顯示的頁面經過方法調用setUserVisibleHint(false)->setUserVisibleHint(true)->onCreateView()...,所以該頁面的數據加載放在onCreateView中;其它預加載頁面預加載時setUserVisibleHint(false)->onCreateView()...,當選中該頁面顯示時調用setUserVisibleHint(true),所以預加載頁面數據加載放在setUserVisibleHint中。

懶加載limit內Fragment的生命周期.png
/**
 * 延遲加載Fragment
 * Created by flying on 2017/3/2.
 */

public abstract class LazyLoadFragment extends BaseFragment {
    protected boolean bIsViewCreated;
    protected boolean bIsDataLoaded;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(getLayoutResId(), container, false);
        initView(view);

        bIsViewCreated = true;

        if (getUserVisibleHint() && !bIsDataLoaded) {
            loadData();
            bIsDataLoaded = true;
        }
        return view;
    }


    @Override
    public void onDestroyView() {
        super.onDestroyView();

        bIsViewCreated = false;
        bIsDataLoaded = false;
    }

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

        if (isVisibleToUser && bIsViewCreated && !bIsDataLoaded) {
            loadData();
            bIsDataLoaded = true;
        }
    }

    /**
     * @return 布局資源id
     */
    protected abstract int getLayoutResId();

    /**
     * 初始化View
     */
    protected abstract void initView(View view);

    /**
     * 加載數據
     */
    protected abstract void loadData();
}

因為懶加載需要設置setOffScreenPageLimit,所以適合有網絡請求、頁面較少且需要緩存的Tab頁面,配合FragmentPagerAdapter使用,因為limit要包括所有的界面,在limit內FragmentStatePagerAdapter和FragmentPagerAdapter沒有區別。

4.ViewPager刷新數據

一般使用PagerAdapter的notifyDataSetChanged方法來刷新數據,但是很多時候數據沒有更新,先來看PagerAdapter的notifyDataSetChanged方法


notifyDataSetChanged.png

觀察者模式:

觀察者模式.png
nofifyChanged.png
onChanged.png

ViewPager中的PagerObserver實現了DateSetObserver


ViewPager_PagerObserver.png

ViewPager中的dataSetChanged方法會根據adapter.getItemPosition返回的值來判斷是否DestroyItem


dataSetChanged.png

getItemPosition默認會返回POSITION_UNCHANGED,而ViewPager中dataSetChanged只有當返回POSITION_NONE時才會銷毀頁面重新創建
getItemPosition.png

繼續看ViewPager中dataSetChanged方法
needPopulate.png

setCurrentItemInternal.png

接著到populate方法


populate.png

終于跑到adapter的instantiateItem方法了
addNewItem.png
FragmentPagerAdapter的inistantiateItem和destoryItem方法.png
FragmentStatePagerAdapter的destoryItem方法.png

所以如果想通過adapter.notifyDataSetChanged來刷新頁面時,必須繼承FragmentStatePagerAdapter,因為FragmentPagerAdapter會緩存Fragment,不會走getItem方法,同時將所要刷新頁面的getItemPosition返回POSITION_NONE

@Override
public int getItemPosition(Object object) {
    return POSITION_NONE;
}

還有其他的一種做法,拿到Fragment,通過Fragment中的public方法來刷新頁面,由FragmentPagerAdapter的instantiateItem方法內部通過tag查找Fragment,因此可以保存其相同的tag

    private SparseArray<String> mTags = new SparseArray<>();
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        mTags.put(position, makeFragmentName(container.getId(), position));
        return super.instantiateItem(container, position);
    }

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

    private String makeFragmentName(int viewId, int position) {
        return "android:switcher:" + viewId + ":" + position;
    }

然后獲取Fragment

Fragment fragment = getSupportFragmentManager().findFragmentByTag(mTags.get(position));
fragment.XXX();

由第二節FragmentStatePagerAdapter的instantiateItem方法可知,其保存時沒有對Fragment添加tag,ViewPager中的Fragment也不能指定id,只有通過調用

  Fragment fragment = (Fragment)(fragmentStatePagerAdapter.instantiateItem(viewpager, position));

來獲取Fragment


參考資料
1.ViewPager ,PagerAdapter,FragmentPagerAdapter,FragmentStatePagerAdapter
2.如何高效的使用ViewPager,以及FragmentPagerAdapter與FragmentStatePagerAdapter的區別
3.FragmentPagerAdapter與FragmentStatePagerAdapter區別
4.死磕Fragment生命周期
5.ViewPager刷新問題詳解

總結

  1. 有Tab時:需要設置setOffScreenPageLimit,FragmentPageAdapter和FragmentStatePageAdapter效果相同,讓Fragment都緩存在內存中,否則Fragment銷毀了再次點擊Tab選中又會重新創建會很突兀。需要網絡請求時則執行延遲加載策略,無需網絡請求時可以正常創建Fragment。

  2. 無Tab時:無需設置SetOffScreenPageLimit,因為默認limit是1,會預加載左右界面,不會顯得突兀。頁面較多時則選用占用內存少的FragmentStatePageAdapter,如瀏覽大圖頁面;頁面較少時則選用加載到內存的FragmentPageAdapter, 如引導頁,需要注意的是FragmentPageAdapter在limit外的Fragment沒有銷毀,生命周期為onPause->onStop->onDestroyView, onCreateView->onStart->onResume,但Fragment的成員變量都沒有變,所以可以緩存根View。

  3. 如果需要刷新所有limit內的頁面,繼承FragmentStatePagerAdapter, 設置getItemPosition返回POSITION_NONE,再調用notifyDataSetChanged;如果只需要刷新單個頁面,則通過獲取Fragment的引用,再通過public方法來更新數據。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容