FragmentPagerAdapter VS FragmentStatePagerAdapter
- FragmentPagerAdapter
Adapter 每頁(yè)都是一個(gè)Fragment,并且所有的Fragment實(shí)例一直保存在Fragment manager中。所以它適用于少量固定的fragment,比如一組用于分頁(yè)顯示的標(biāo)簽。除了當(dāng)Fragment不可見(jiàn)時(shí),它的視圖層有可能被銷(xiāo)毀外,每頁(yè)的Fragment都會(huì)被保存在內(nèi)存中。
經(jīng)過(guò)測(cè)試我們可以發(fā)現(xiàn),當(dāng)使用 FragmentPagerAdapter 的時(shí)候已經(jīng)被創(chuàng)建的頁(yè)面變?yōu)椴豢梢?jiàn)的時(shí)候生命周期為:
onPause -> onStop -> onDestroyView
- FragmentStatePagerAdapter
每頁(yè)都是一個(gè)Fragment,當(dāng)Fragment不被需要時(shí)(比如不可見(jiàn)),整個(gè)Fragment都會(huì)被銷(xiāo)毀,除了saved state被保存外(保存下來(lái)的bundle用于恢復(fù)Fragment實(shí)例)。所以它適用于很多頁(yè)的情況。
經(jīng)過(guò)測(cè)試我們可以發(fā)現(xiàn),當(dāng)使用 FragmentPagerAdapter 的時(shí)候已經(jīng)被創(chuàng)建的頁(yè)面變?yōu)椴豢梢?jiàn)的時(shí)候生命周期為:
onPause -> onStop -> onDestoryView -> onDestroy -> onDetach
可以看出當(dāng)使用 FragmentStatePagerAdapter 的時(shí)候,不會(huì)保留不可見(jiàn)(不相鄰的)頁(yè)面在內(nèi)存中,而是直接銷(xiāo)毀了,等下次在可見(jiàn)(相鄰頁(yè)面可見(jiàn))的時(shí)候再次創(chuàng)建。
FragmentPagerAdapter 中 Fragment 的生命周期( setUserVisibleHint 亂入)
我們知道 Fragment 生命周期函數(shù)有 :
setUserVisibleHint(亂入的不用在意我) -> onAttach —> onCreate -> onCreateView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDetach -> onDestroy
Fragment 本身作為片段存在于 Activity中,當(dāng) Fragment 單獨(dú)使用的時(shí)候其生命周期是伴隨著 Activity 的生命周期的,即 Activity onResume 的時(shí)候 內(nèi)部所有的 Fragment 也就走到了 onResume 這點(diǎn)初學(xué)者可能不會(huì)太在意,在這里作為題外話敲下黑板。
想要進(jìn)一步了解的請(qǐng)參照: 與 Activity 生命周期協(xié)調(diào)一致
[站外圖片上傳中...(image-489935-1517594338763)]
下面說(shuō)用 FragmentPagerAdapter 承載頁(yè)面的 Fragment 時(shí)候?qū)?yīng)的生命周期。這里說(shuō)的情況都是沒(méi)有設(shè)置 setOffscreenPageLimit 屬性的時(shí)候。這里假設(shè) ViewPager 中有3個(gè) Fragment :OutAFragment ,OutBFragment,OutCFragment 。當(dāng)首次進(jìn)入 Activity 時(shí)候 ViewPager 會(huì)初始化 OutAFragment,并預(yù)加載 OutBFragment,生命周期如下所示:
// 首次進(jìn)入界面 A
E/TAG: OutAFragment unVisibleToUser
E/TAG: OutBFragment unVisibleToUser
E/TAG: OutAFragment isVisibleToUser
E/TAG: OutAFragment onAttach
E/TAG: OutAFragment onCreate
E/TAG: OutBFragment onAttach
E/TAG: OutBFragment onCreate
E/TAG: OutAFragment onCreateView
E/TAG: OutAFragment onActivityCreated
E/TAG: OutAFragment onStart
E/TAG: OutAFragment onResume
E/TAG: OutBFragment onCreateView
E/TAG: OutBFragment onActivityCreated
E/TAG: OutBFragment onStart
E/TAG: OutBFragment onResume
通過(guò)上方的日志打印我們可以清楚的看到,OutAFragment,OutBFragment 都伴隨著 Activity 的生命周期走了對(duì)應(yīng)的生命周期方法。
接下來(lái)我們依次滑動(dòng)到 OutBFragment 和 OutCFragment 看下生命周期方法是怎么走的:
// 首次進(jìn)入界面 B 因?yàn)?ViewPager 對(duì)界面進(jìn)行了緩存造成了
E/TAG: OutCFragment unVisibleToUser
E/TAG: OutAFragment unVisibleToUser
E/TAG: OutBFragment isVisibleToUser
E/TAG: OutCFragment onAttach
E/TAG: OutCFragment onCreate
E/TAG: OutCFragment onCreateView
E/TAG: OutCFragment onActivityCreated
E/TAG: OutCFragment onStart
E/TAG: OutCFragment onResume
//首次進(jìn)入界面 C
E/TAG: OutBFragment unVisibleToUser
E/TAG: OutCFragment isVisibleToUser
E/TAG: OutAFragment onPause
E/TAG: OutAFragment onStop
E/TAG: OutAFragment onDestroyView
不知道這里大家注意到了沒(méi)有由于 ViewPager 對(duì)相鄰頁(yè)面做了預(yù)加載處理,造成我們切換到 B 頁(yè)面的時(shí)候 OutBFragment 沒(méi)有走任何生命周期函數(shù)。只調(diào)用了 setUserVisibleHint 并傳入 true 正如上述 Log 打印一樣。 當(dāng)我們滑到 C 的時(shí)候 OutAFragment View 被回收,但 Fragment 對(duì)象本身并沒(méi)有被回收。
上述只是說(shuō)明了當(dāng)我們順序滑動(dòng)的時(shí)候片段的生命周期調(diào)用,如果我們以 A —>C 的方式進(jìn)行點(diǎn)擊切換生命周期就會(huì)發(fā)生變化。此時(shí)我們可以理解為 C 并沒(méi)被預(yù)加載而是直接初始化了,所以他不會(huì)像先前的一樣只調(diào)用 setUserVisibleHint 而是:
E/TAG: OutCFragment unVisibleToUser
E/TAG: OutAFragment unVisibleToUser
E/TAG: OutCFragment isVisibleToUser
E/TAG: OutCFragment onAttach
E/TAG: OutCFragment onCreate
E/TAG: OutCFragment onCreateView
E/TAG: OutCFragment onActivityCreated
E/TAG: OutCFragment onStart
E/TAG: OutCFragment onResume
E/TAG: OutAFragment onPause
E/TAG: OutAFragment onStop
E/TAG: OutAFragment onDestroyView
這樣生命周期大概就解釋完了。那么我們仔細(xì)想一下,我們是不是不能像 Activity 一樣簡(jiǎn)單的在 onResume 中調(diào)用網(wǎng)絡(luò)請(qǐng)求就可以實(shí)現(xiàn)界面可見(jiàn)的時(shí)候刷新了? 而對(duì)于產(chǎn)品來(lái)說(shuō)這種要求并不過(guò)分,相信一部分人已經(jīng)想到了,那么就是 Fragment 與 ViewPager 連用時(shí)的 網(wǎng)絡(luò)數(shù)據(jù)懶加載。
Fragment 與 ViewPager 連用時(shí)的 網(wǎng)絡(luò)數(shù)據(jù)懶加載
關(guān)于在 ViewPager 中為什么需要懶加載,當(dāng)然是因?yàn)?ViewPager 自身會(huì)幫我們緩存相鄰 pager 的 Fragment ,Android 本身為我們做了這件事,是為了提高 ViewPager 滑動(dòng)的流暢度,給用戶(hù)帶來(lái)更好的體驗(yàn)。那么懶加載應(yīng)用的場(chǎng)景是什么呢?
當(dāng) ViewPager 中的 Fragment 內(nèi)部有網(wǎng)絡(luò)請(qǐng)求的時(shí)候,展示數(shù)據(jù)依賴(lài)于網(wǎng)絡(luò)請(qǐng)求結(jié)果的時(shí)候,我們就應(yīng)該考慮使用懶加載方案。
試想一下,你現(xiàn)在有多個(gè)頁(yè)面包含在 Viewpager 中,并且頁(yè)面中有大量數(shù)據(jù)需要展示,提前加載這些數(shù)據(jù)勢(shì)必會(huì)對(duì)用戶(hù)造成不必要的流量損失,也會(huì)損失一部分內(nèi)存。
網(wǎng)上懶加載的例子很多,但是思想都是一樣的利用 「setUserVisibleHint」來(lái)完成的,下邊簡(jiǎn)單說(shuō)下我的實(shí)現(xiàn)方法。
創(chuàng)建 BaseLazyLoadFragment
創(chuàng)建 BaseLoadFragment 子類(lèi)只需要實(shí)現(xiàn)對(duì)應(yīng)網(wǎng)絡(luò)請(qǐng)求方法,Base 類(lèi)負(fù)責(zé)請(qǐng)求執(zhí)行的時(shí)機(jī)。上文說(shuō)到懶加載主要通過(guò) 「setUserVisibleHint」方法來(lái)判斷對(duì)應(yīng)的 Fragment 是否是用戶(hù)正在交互的片段。
對(duì)于 FragmentPagerAdapter 中所有 Fragment ,setUserVisibleHint 調(diào)用時(shí)機(jī)是在所有的生命周期之前,對(duì)于當(dāng)前所處的 pager ,
isVisibleToUser = true,預(yù)加載的 pagerisVisibleToUser = false。由于 setUserVisibleHint 調(diào)用在生命周期之前,所以貿(mào)然在
isVisibleToUser = true時(shí)候去請(qǐng)求數(shù)據(jù),當(dāng)網(wǎng)絡(luò)回調(diào)回來(lái)的時(shí)候可能會(huì)導(dǎo)致頁(yè)面沒(méi)初始化完畢,而造成設(shè)置數(shù)據(jù)的時(shí)候空指針的現(xiàn)象。解決方法當(dāng)然是有的我們可以添加 view 是否創(chuàng)建完成的判斷,如下面所示,我們?cè)?inflate 完成 view 后將
onCreatedView標(biāo)志位置位,那么被緩存的頁(yè)面下次在進(jìn)入的時(shí)候就可以在setUserVisibleHint中開(kāi)始網(wǎng)絡(luò)請(qǐng)求。那么對(duì)于沒(méi)有被緩存的頁(yè)面就會(huì)在
onCreatedView中進(jìn)行網(wǎng)絡(luò)請(qǐng)求。
你以為我現(xiàn)在肯定要放代碼了,to naive ! 對(duì)于懶加載我們還應(yīng)該考慮到實(shí)際需求,如有些界面我們只需要在頁(yè)面第一次顯示的時(shí)候請(qǐng)求數(shù)據(jù),當(dāng) Fragment 下次在可見(jiàn)的時(shí)候需要用戶(hù)手動(dòng)去刷新界面,當(dāng)然這個(gè)需求是最常見(jiàn)的,也能滿(mǎn)足大部分 Feed 流應(yīng)用的需求??墒怯行?shí)時(shí)要求比較高的頁(yè)面,比如股票類(lèi)應(yīng)用的持倉(cāng)頁(yè)面,錢(qián)數(shù)每分每秒都在變化那么只是首次進(jìn)入刷新是遠(yuǎn)遠(yuǎn)不夠的,這時(shí)候就需要每次用戶(hù)可見(jiàn)的時(shí)候都刷新一次,當(dāng)然這是舉個(gè)例子,實(shí)際上在真正的股票類(lèi)應(yīng)用持倉(cāng)頁(yè)是 socket 刷新的。所以面對(duì)千變?nèi)f化的需求就需要我們的懶加載基類(lèi)能夠支撐的起這兩種方案,我們可以暴露兩個(gè)方法給子類(lèi) Fragment
- 如果需求僅僅是想要在第一可見(jiàn)的時(shí)候自動(dòng)刷新 就調(diào)用 requestData
- 如果用戶(hù)想要每次可見(jiàn)的時(shí)候都刷新,那么就調(diào)用 requestDataAutoRefresh
這樣這兩種需求都滿(mǎn)足了。只需要根據(jù)指定頁(yè)面復(fù)寫(xiě)滿(mǎn)足需求的方法就好了。下面來(lái)看具體代碼:
public abstract class LazyLoadBaseFragment extends Fragment {
public static final String TAG = "Fragment";
private View rootView = null;
private boolean isViewCreated;
private boolean isFirstVisible = true;
private boolean isFragmentVisible;
/**
* 在 FragmentPageAdapter 中的 Fragment 都會(huì)走這里兩次, 且比任何生命周期都要先走
* 1. fragment 被預(yù)加載 此時(shí)為 false ,切換到此 Fragment 再次走這里賦值為 true
* 2. 跨 tab 切換時(shí)候 首先也會(huì)走一次 false 然后去執(zhí)行跳轉(zhuǎn)之前 tab 的 setUserVisibleHint(false) 后去執(zhí)行臨近(左右) fragment 的
* setUserVisibleHint(false) 最后會(huì)在調(diào)用一次當(dāng)前 tab 的 setUserVisibleHint(true)
* <p>
* 懶加載是為了用戶(hù)能在頁(yè)面可見(jiàn)的時(shí)候在再去請(qǐng)求數(shù)據(jù)
* <p>
* 實(shí)際需求有:
* 1. 用戶(hù)第一可見(jiàn)自動(dòng)加載數(shù)據(jù),之后需要用戶(hù)手動(dòng)刷新去加載數(shù)據(jù)
* 2. 用戶(hù)每次可見(jiàn)的時(shí)候去自動(dòng)刷新最新數(shù)據(jù)
* <p>
* <p>
* 分析;
* 我們可能認(rèn)為Adapter 中的 Fragment 和 Activity 一樣每次用戶(hù)可見(jiàn)去調(diào)用 onResume 事實(shí)上并不是這樣的,
* 由于 FragmentPagerAdapter 存在預(yù)加載,onResume 事件當(dāng)預(yù)加載的時(shí)候已經(jīng)執(zhí)行了,如果僅考慮兩個(gè) tab 之間相互
* 切換那么 這兩個(gè) Fragment 之后只會(huì)調(diào)用 setUserVisibleHint 當(dāng)參數(shù)為 true 的時(shí)候 Fragment 可見(jiàn),false 的時(shí)候不可見(jiàn)。
* <p>
* 那么回到上述需求:我們可以提供兩個(gè)方法 requestData requestDataEveryTime
* <p>
* 1. 如果需求僅僅是想要在第一可見(jiàn)的時(shí)候自動(dòng)刷新 就調(diào)用 requestData
* 2. 如果用戶(hù)想要每次可見(jiàn)的時(shí)候都刷新,那么就調(diào)用 requestDataAutoRefresh
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
isFragmentVisible = isVisibleToUser;
//當(dāng) View 創(chuàng)建完成切 用戶(hù)可見(jiàn)的時(shí)候請(qǐng)求 且僅當(dāng)是第一次對(duì)用戶(hù)可見(jiàn)的時(shí)候請(qǐng)求自動(dòng)數(shù)據(jù)
if (isVisibleToUser && isViewCreated && isFirstVisible) {
Log.e(TAG, "只有自動(dòng)請(qǐng)求一次數(shù)據(jù) requestData");
requestData();
requestDataAutoRefresh();
isFirstVisible = false;
}
// 由于每次可見(jiàn)都需要刷新所以我們只需要判斷 Fragment 展示在用戶(hù)面面前了,view 初始化完成了 然后即可以請(qǐng)求數(shù)據(jù)了
if (isVisibleToUser && isViewCreated) {
// Log.e(TAG, "每次都可見(jiàn)數(shù)據(jù) requestDataAutoRefresh");
requestDataAutoRefresh();
}
if (!isVisibleToUser && isViewCreated) {
stopRefresh();
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
if (rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false);
}
isViewCreated = true;
initView(rootView);
//Adapter 默認(rèn)展示的那個(gè) Fragment ,或者隔 tab 選中的時(shí)候,
//由于沒(méi)有預(yù)加載tab 在 setUserVisibleHint 中 isVisibleToUser = true 的時(shí)候 view 還沒(méi)創(chuàng)建成功
//即 isViewCreated = false 所以該 tab 的請(qǐng)求延遲到了 view 創(chuàng)建完成了
//但是已經(jīng)緩存的 Fragment 就不可以走這里了因?yàn)轭A(yù)加載 Fragment 在 onCreateView 中 isFragmentVisible 為 false
if (isFragmentVisible && isFirstVisible) {
Log.e(TAG, "Adapter 默認(rèn)展示的那個(gè) Fragment ,或者隔 tab 選中的時(shí)候 requestData 推遲到 onCreateView 后 ");
requestData();
requestDataAutoRefresh();
isFirstVisible = false;
}
return rootView;
}
/**
* 只有在 Fragment 第一次對(duì)用戶(hù)可見(jiàn)的時(shí)候才去請(qǐng)求
*/
protected void requestData() {
}
/**
* 每次 Fragment 對(duì)用戶(hù)可見(jiàn)都會(huì)去請(qǐng)求
*/
protected void requestDataAutoRefresh() {
}
/**
* 當(dāng) Fragment 不可見(jiàn)的時(shí)候停止某些輪詢(xún)請(qǐng)求的時(shí)候調(diào)用該方法停止請(qǐng)求
*/
protected void stopRefresh() {
}
/**
* 返回布局 resId
*
* @return layoutId
*/
protected abstract int getLayoutRes();
/**
* 初始化view
*
* @param rootView
*/
protected abstract void initView(View rootView);
}
小結(jié)
我們這篇文章分享了 ViewPager 中 Fragment 的生命周期,以及如何實(shí)現(xiàn) Fragment 與 ViewPager 連用時(shí)的 網(wǎng)絡(luò)數(shù)據(jù)懶加載,下一篇我將會(huì)說(shuō)明 Fragment 嵌套,以及 FragmentAdapter 嵌套 FragmentAdapter 時(shí)的生命周期與注意事項(xiàng)。 本文 github 鏈接 https://github.com/ImportEffort/FragmentLiftCycle