前面兩篇文章中,對(duì) Fragment 的基本使用、常見問題和狀態(tài)恢復(fù)做了詳細(xì)的分析總結(jié)。除了在 Activity 中單獨(dú)使用 Fragment,F(xiàn)ragment + ViewPager 組合也是項(xiàng)目中使用非常頻繁的方式,本文再來總結(jié)一下這種組合使用時(shí)的注意事項(xiàng)。在此之前,如果你對(duì) Fragment 的認(rèn)知和使用還有不清楚的地方,一定要先閱讀前面兩篇文章:
基本使用
對(duì)于這種組合使用,ViewPager 提供了兩種頁(yè)面適配器來管理不同 Fragment 之間的滑動(dòng)切換: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();
}
}
如上述代碼所示,沒有特別要求的話,無論是哪種適配器類,實(shí)現(xiàn)起來都比簡(jiǎn)單,不需要像普通的 ViewPager + View 組合那樣,還需處理視圖的初始化工作(instantiateItem
方法)和銷毀(destroyItem
方法)等。FragmentPagerAdapter
和 FragmentStatePagerAdapter
在內(nèi)部已經(jīng)默認(rèn)實(shí)現(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
時(shí),ViewPager 中的所有 Fragment 實(shí)例常駐內(nèi)存,當(dāng) Fragment 變得不可見時(shí)僅僅是視圖結(jié)構(gòu)的銷毀,即調(diào)用了 onDestroyView
方法。由于 FragmentPagerAdapter
內(nèi)存消耗較大,所以適合少量靜態(tài)頁(yè)面的場(chǎng)景。
使用 FragmentStatePagerAdapter
時(shí),當(dāng) Fragment 變得不可見,不僅視圖層次銷毀,實(shí)例也被銷毀,即調(diào)用了 onDestroyView
和 onDestroy
方法,僅僅保存 Fragment 狀態(tài)。相比而言, FragmentStatePagerAdapter
內(nèi)存占用較小,所以適合大量動(dòng)態(tài)頁(yè)面,比如我們常見的新聞列表類應(yīng)用。
“Talk is cheap, show me the code.” 如果這樣表達(dá)還是不能理解二者之間的區(qū)別的話,最好的辦法就是用代碼來表達(dá)。
新建一個(gè)名為 BaseFragment 的基類,繼承自 Fragment,重寫
setUserVisibleHint
和 各個(gè)生命周期函數(shù),添加日志打印。然后新建四個(gè)子類,分別命名為 OneFragment、TwoFragment、ThreeFragment 和 FourFragment,按照基本使用方法寫好代碼,運(yùn)行并滑動(dòng)頁(yè)面,查看日志打印。
由于代碼較為簡(jiǎn)單,考慮內(nèi)容長(zhǎng)度,這里就不貼相關(guān)代碼,主要描述思想。對(duì)應(yīng)日志截圖如下:
使用 FragmentPagerAdapter
時(shí):
使用 FragmentStatePagerAdapter
時(shí):
圖中做了相應(yīng)標(biāo)記說明,二者區(qū)別一目了然,無需過多解釋。出現(xiàn)這樣的區(qū)別,其實(shí)從源碼中的 instantiateItem
和 destroyItem
也能讀出一二,感興趣的話可以翻看一下。
Fragment 懶加載
懶加載,顧名思義,是希望在展示相應(yīng) Fragment 頁(yè)面時(shí)再動(dòng)態(tài)加載頁(yè)面數(shù)據(jù),數(shù)據(jù)通常來自于網(wǎng)絡(luò)或本地?cái)?shù)據(jù)庫(kù)。這種做法的合理性在于用戶可能不會(huì)滑到一下頁(yè)面,同時(shí)還能幫助減輕當(dāng)前頁(yè)面數(shù)據(jù)請(qǐng)求的帶寬壓力,如果是用戶使用流量的話,還能避免無用的流量消耗。
從上面的截圖中可以看出,ViewPager 在展示當(dāng)前頁(yè)面時(shí),會(huì)同時(shí)預(yù)加載下一頁(yè)面。事實(shí)上,可以通過 ViewPager 提供的 setOffscreenPageLimit(int limit)
方法設(shè)置 ViewPager 預(yù)加載的頁(yè)面數(shù)量,默認(rèn)值為 1,并且這個(gè)參數(shù)的值不能小于 1。所以也就無法通過這個(gè)方法實(shí)現(xiàn) ViewPager 中 Fragment 的懶加載,一定要改 ViewPager 的話只能通過自定義一個(gè) Viewpager 類來實(shí)現(xiàn),這種做法就比較繁瑣。其實(shí)可以從 Fragment 下手。
ViewPager 本質(zhì)上是通過 Fragment 調(diào)用 setUserVisibleHint
方法實(shí)現(xiàn) Fragment 頁(yè)面的展示與隱藏,這一點(diǎn)從FragmentPagerAdapter
和 FragmentStatePagerAdapter
的源碼和上面的截圖中都可以看出。那么對(duì)應(yīng)的解決方案就有了,自定義一個(gè) LazyLoadFragment 基類,利用 setUserVisibleHint
和 生命周期方法,通過對(duì) Fragment 狀態(tài)判斷,進(jìn)行數(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 中實(shí)現(xiàn) requestData 方法即可。這里添加了一個(gè) isDataLoaded 變量,目的是避免重復(fù)加載數(shù)據(jù)。考慮到有時(shí)候需要刷新數(shù)據(jù)的問題,便提供了一個(gè)用于強(qiáng)制刷新的參數(shù)判斷。這種思路來自于 這篇文章,在此基礎(chǔ)上做了一些修改。實(shí)際上,在項(xiàng)目開發(fā)過程中,還需處理網(wǎng)絡(luò)請(qǐng)求失敗等特殊情況,我想,了解原理之后,這些問題都不再是問題。
Fragment 狀態(tài)恢復(fù)問題
前文描述FragmentPagerAdapter
與 FragmentStatePagerAdapter
的區(qū)別時(shí)有提到,這兩種適配器類默認(rèn)都會(huì)保存 Fragment 狀態(tài),包括 View 狀態(tài)和成員變量數(shù)據(jù)狀態(tài)。需要注意的是,View 狀態(tài)包括的內(nèi)容很多,比如用戶在 EditText 中輸入的內(nèi)容、ScrollView 滑動(dòng)的位置紀(jì)錄等。
有關(guān) Fragment 的具體使用細(xì)節(jié)和注意事項(xiàng)可以參考這篇文章:Android Activity 和 Fragment 狀態(tài)保存與恢復(fù)的最佳實(shí)踐。這里我說說另外兩種簡(jiǎn)單粗暴的做法。
一種是通過 setOffscreenPageLimit
方法設(shè)置保留視圖結(jié)構(gòu)的 Fragment 數(shù)量,比較簡(jiǎn)單,比如保留所有 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)就不會(huì)銷毀,換一種角度解決了 Fragment 狀態(tài)保存與恢復(fù)的問題。
可以看出,使用這兩種處理方式時(shí),F(xiàn)ragment 實(shí)例均保存在內(nèi)存中,具有一定內(nèi)存消耗,適合于頁(yè)面較少的情況。至于大量頁(yè)面,還是推薦通過 Fragment 自帶的狀態(tài)保存與恢復(fù)方式處理。