前面兩篇文章中,對 Fragment 的基本使用截歉、常見問題和狀態(tài)恢復(fù)做了詳細(xì)的分析總結(jié)胖腾。除了在 Activity 中單獨使用 Fragment,F(xiàn)ragment + ViewPager 組合也是項目中使用非常頻繁的方式瘪松,本文再來總結(jié)一下這種組合使用時的注意事項咸作。在此之前,如果你對 Fragment 的認(rèn)知和使用還有不清楚的地方宵睦,一定要先閱讀前面兩篇文章:
基本使用
對于這種組合使用,ViewPager 提供了兩種頁面適配器來管理不同 Fragment 之間的滑動切換:FragmentPagerAdapter
和 FragmentStatePagerAdapter
壳嚎。先來看一下他們的基本使用桐智,稍后再分析二者之間的區(qū)別。
// private class ContentPagerAdapter extends FragmentStatePagerAdapter{
private class ContentPagerAdapter extends FragmentPagerAdapter{
public ContentPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
}
如上述代碼所示烟馅,沒有特別要求的話说庭,無論是哪種適配器類,實現(xiàn)起來都比簡單郑趁,不需要像普通的 ViewPager + View 組合那樣刊驴,還需處理視圖的初始化工作(instantiateItem
方法)和銷毀(destroyItem
方法)等。FragmentPagerAdapter
和 FragmentStatePagerAdapter
在內(nèi)部已經(jīng)默認(rèn)實現(xiàn)了這些功能寡润。
兩種 PagerAdapter 區(qū)別
源碼定義中已經(jīng)很清楚地描述了 FragmentPagerAdapter
和 FragmentStatePagerAdapter
的區(qū)別:
FragmentPagerAdapter
Implementation of PagerAdapter that represents each page as a Fragment that is persistently kept in the fragment manager as long as the user can return to the page.
This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider FragmentStatePagerAdapter.
FragmentStatePagerAdapter
Implementation of PagerAdapter that uses a Fragment to manage each page. This class also handles saving and restoring of fragment's state.
This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to FragmentPagerAdapter at the cost of potentially more overhead when switching between pages.
總結(jié)歸納如下:
使用 FragmentPagerAdapter
時捆憎,ViewPager 中的所有 Fragment 實例常駐內(nèi)存,當(dāng) Fragment 變得不可見時僅僅是視圖結(jié)構(gòu)的銷毀梭纹,即調(diào)用了 onDestroyView
方法躲惰。由于 FragmentPagerAdapter
內(nèi)存消耗較大,所以適合少量靜態(tài)頁面的場景栗柒。
使用 FragmentStatePagerAdapter
時礁扮,當(dāng) Fragment 變得不可見知举,不僅視圖層次銷毀瞬沦,實例也被銷毀,即調(diào)用了 onDestroyView
和 onDestroy
方法雇锡,僅僅保存 Fragment 狀態(tài)逛钻。相比而言, FragmentStatePagerAdapter
內(nèi)存占用較小锰提,所以適合大量動態(tài)頁面曙痘,比如我們常見的新聞列表類應(yīng)用芳悲。
“Talk is cheap, show me the code.” 如果這樣表達還是不能理解二者之間的區(qū)別的話,最好的辦法就是用代碼來表達边坤。
新建一個名為 BaseFragment 的基類名扛,繼承自 Fragment,重寫
setUserVisibleHint
和 各個生命周期函數(shù)茧痒,添加日志打印肮韧。然后新建四個子類,分別命名為 OneFragment旺订、TwoFragment弄企、ThreeFragment 和 FourFragment,按照基本使用方法寫好代碼区拳,運行并滑動頁面拘领,查看日志打印。
由于代碼較為簡單樱调,考慮內(nèi)容長度约素,這里就不貼相關(guān)代碼,主要描述思想本涕。對應(yīng)日志截圖如下:
使用 FragmentPagerAdapter
時:
使用 FragmentStatePagerAdapter
時:
圖中做了相應(yīng)標(biāo)記說明业汰,二者區(qū)別一目了然,無需過多解釋菩颖。出現(xiàn)這樣的區(qū)別样漆,其實從源碼中的 instantiateItem
和 destroyItem
也能讀出一二,感興趣的話可以翻看一下晦闰。
Fragment 懶加載
懶加載放祟,顧名思義,是希望在展示相應(yīng) Fragment 頁面時再動態(tài)加載頁面數(shù)據(jù)呻右,數(shù)據(jù)通常來自于網(wǎng)絡(luò)或本地數(shù)據(jù)庫跪妥。這種做法的合理性在于用戶可能不會滑到一下頁面,同時還能幫助減輕當(dāng)前頁面數(shù)據(jù)請求的帶寬壓力声滥,如果是用戶使用流量的話眉撵,還能避免無用的流量消耗。
從上面的截圖中可以看出落塑,ViewPager 在展示當(dāng)前頁面時纽疟,會同時預(yù)加載下一頁面。事實上憾赁,可以通過 ViewPager 提供的 setOffscreenPageLimit(int limit)
方法設(shè)置 ViewPager 預(yù)加載的頁面數(shù)量污朽,默認(rèn)值為 1,并且這個參數(shù)的值不能小于 1龙考。所以也就無法通過這個方法實現(xiàn) ViewPager 中 Fragment 的懶加載蟆肆,一定要改 ViewPager 的話只能通過自定義一個 Viewpager 類來實現(xiàn)矾睦,這種做法就比較繁瑣。其實可以從 Fragment 下手炎功。
ViewPager 本質(zhì)上是通過 Fragment 調(diào)用 setUserVisibleHint
方法實現(xiàn) Fragment 頁面的展示與隱藏枚冗,這一點從FragmentPagerAdapter
和 FragmentStatePagerAdapter
的源碼和上面的截圖中都可以看出。那么對應(yīng)的解決方案就有了蛇损,自定義一個 LazyLoadFragment 基類官紫,利用 setUserVisibleHint
和 生命周期方法,通過對 Fragment 狀態(tài)判斷州藕,進行數(shù)據(jù)加載束世,并將數(shù)據(jù)加載的接口提供開放出去,供子類使用床玻。參考代碼如下:
public abstract class LazyLoadFragment extends BaseFragment {
protected boolean isViewInitiated;
protected boolean isDataLoaded;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewInitiated = true;
prepareRequestData();
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
prepareRequestData();
}
public abstract void requestData();
public boolean prepareRequestData() {
return prepareRequestData(false);
}
public boolean prepareRequestData(boolean forceUpdate) {
if (getUserVisibleHint() && isViewInitiated && (!isDataLoaded || forceUpdate)) {
requestData();
isDataLoaded = true;
return true;
}
return false;
}
}
然后在子類 Fragment 中實現(xiàn) requestData 方法即可毁涉。這里添加了一個 isDataLoaded 變量,目的是避免重復(fù)加載數(shù)據(jù)锈死∑堆撸考慮到有時候需要刷新數(shù)據(jù)的問題,便提供了一個用于強制刷新的參數(shù)判斷待牵。這種思路來自于 這篇文章其屏,在此基礎(chǔ)上做了一些修改。實際上缨该,在項目開發(fā)過程中偎行,還需處理網(wǎng)絡(luò)請求失敗等特殊情況,我想贰拿,了解原理之后蛤袒,這些問題都不再是問題。
Fragment 狀態(tài)恢復(fù)問題
前文描述FragmentPagerAdapter
與 FragmentStatePagerAdapter
的區(qū)別時有提到膨更,這兩種適配器類默認(rèn)都會保存 Fragment 狀態(tài)妙真,包括 View 狀態(tài)和成員變量數(shù)據(jù)狀態(tài)。需要注意的是荚守,View 狀態(tài)包括的內(nèi)容很多珍德,比如用戶在 EditText 中輸入的內(nèi)容、ScrollView 滑動的位置紀(jì)錄等矗漾。
有關(guān) Fragment 的具體使用細(xì)節(jié)和注意事項可以參考這篇文章:Android Activity 和 Fragment 狀態(tài)保存與恢復(fù)的最佳實踐锈候。這里我說說另外兩種簡單粗暴的做法。
一種是通過 setOffscreenPageLimit
方法設(shè)置保留視圖結(jié)構(gòu)的 Fragment 數(shù)量缩功,比較簡單晴及,比如保留所有 Fragment 視圖結(jié)構(gòu): mContentVp.getAdapter().getCount()-1都办;另一種就是重寫適配器的 Fragment 相關(guān)方法嫡锌,比如:
// private class ContentPagerAdapter extends FragmentStatePagerAdapter {
private class ContentPagerAdapter extends FragmentPagerAdapter {
private FragmentManager fragmentManager;
public ContentPagerAdapter(FragmentManager fm) {
super(fm);
this.fragmentManager = fm;
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
this.fragmentManager.beginTransaction().show(fragment).commit();
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = fragmentList.get(position);
fragmentManager.beginTransaction().hide(fragment).commit();
}
}
這種處理下虑稼,也就不用區(qū)分使用的是哪種適配器類,通過重寫 instantiateItem
和 destroyItem
方法势木,使用 show 和 hide 方法處理 Fragment 的展示與隱藏蛛倦,這樣,視圖結(jié)構(gòu)就不會銷毀啦桌,換一種角度解決了 Fragment 狀態(tài)保存與恢復(fù)的問題溯壶。
可以看出,使用這兩種處理方式時甫男,F(xiàn)ragment 實例均保存在內(nèi)存中且改,具有一定內(nèi)存消耗,適合于頁面較少的情況板驳。至于大量頁面又跛,還是推薦通過 Fragment 自帶的狀態(tài)保存與恢復(fù)方式處理。