FragmentPagerAdapter與FragmentStatePagerAdapter區(qū)別
轉:http://www.apkbus.com/android-90417-1-1.html
在一個 Android 應用中汰蓉,我使用 FragmentPagerAdapter 來處理多 Fragment 頁面的橫向滑動。不過我碰到了一個問題,即當 Fragment 對應的數(shù)據(jù)集發(fā)生改變時技掏,我希望能夠通過調(diào)用 mAdapter.notifyDataSetChanged() 來觸發(fā) Fragment 頁面使用新的數(shù)據(jù)調(diào)整或重新生成其內(nèi)容吓肋,可是當我調(diào)用 notifyDataSetChanged() 后思灌,發(fā)現(xiàn)什么都沒發(fā)生蝉揍。搜索之后發(fā)現(xiàn)不止我一個人碰到這個問題趋箩,大家給出的解決辦法五花八門啃炸,有些確實解決了問題铆隘,但是我總感覺問題沒搞清楚。于是我決定搞明白這個問題到底是怎么回事南用,以及正確的用法到底如何膀钠。要搞明白這個問題,僅僅閱讀文檔并不足夠裹虫,還需要閱讀相關幾個類的相關方法的實現(xiàn)肿嘲,搞懂其設計意圖。下面就是通過閱讀源代碼搞明白的內(nèi)容筑公。
【ViewPager】
ViewPager 如其名所述雳窟,是負責翻頁的一個 View。準確說是一個 ViewGroup匣屡,包含多個 View 頁封救,在手指橫向滑動屏幕時,其負責對 View 進行切換捣作。為了生成這些 View 頁誉结,需要提供一個 PagerAdapter 來進行和數(shù)據(jù)綁定以及生成最終的 View 頁。
setAdapter()
ViewPager 通過 setAdapter() 來建立與 PagerAdapter 的聯(lián)系虾宇。這個聯(lián)系是雙向的搓彻,一方面,ViewPager 會擁有 PagerAdapter 對象嘱朽,從而可以在需要時調(diào)用 PagerAdapter 的方法旭贬;另一方面,ViewPager 會在 setAdapter() 中調(diào)用 PagerAdapter 的 registerDataSetObserver() 方法搪泳,注冊一個自己生成的 PagerObserver 對象稀轨,從而在 PagerAdapter 有所需要時(如 notifyDataSetChanged()或 notifyDataSetInvalidated() 時),可以調(diào)用 Observer 的 onChanged() 或 onInvalidated() 方法岸军,從而實現(xiàn) PagerAdapter 向 ViewPager 方向發(fā)送信息奋刽。dataSetChanged()
在 PagerObserver.onChanged()瓦侮,以及 PagerObserver.onInvalide() 中被調(diào)用。因此當 PagerAdapter.notifyDataSetChanged() 被觸發(fā)時佣谐,ViewPager.dataSetChanged() 也可以被觸發(fā)肚吏。該函數(shù)將使用 getItemPosition() 的返回值來進行判斷,如果為 POSITION_UNCHANGED狭魂,則什么都不做罚攀;如果為 POSITION_NONE,則調(diào)用 PagerAdapter.destroyItem() 來去掉該對象雌澄,并設置為需要刷新 (needPopulate = true) 以便觸發(fā)PagerAdapter.instantiateItem() 來生成新的對象斋泄。
【PagerAdapter】
PageAdapter 是 ViewPager 的支持者,ViewPager 將調(diào)用它來取得所需顯示的頁镐牺,而 PageAdapter 也會在數(shù)據(jù)變化時炫掐,通知 ViewPager。這個類也是FragmentPagerAdapter 以及 FragmentStatePagerAdapter 的基類睬涧。如果繼承自該類募胃,至少需要實現(xiàn) instantiateItem(), destroyItem(), getCount() 以及 isViewFromObject()。
getItemPosition()該函數(shù)用以返回給定對象的位置宙地,給定對象是由 instantiateItem() 的返回值摔认。
在 ViewPager.dataSetChanged() 中將對該函數(shù)的返回值進行判斷,以決定是否最終觸發(fā) PagerAdapter.instantiateItem() 函數(shù)宅粥。
在 PagerAdapter 中的實現(xiàn)是直接傳回 POSITION_UNCHANGED参袱。如果該函數(shù)不被重載,則會一直返回 POSITION_UNCHANGED秽梅,從而導致 ViewPager.dataSetChanged() 被調(diào)用時抹蚀,認為不必觸發(fā) PagerAdapter.instantiateItem()。很多人因為沒有重載該函數(shù)企垦,而導致調(diào)用PagerAdapter.notifyDataSetChanged() 后环壤,什么都沒有發(fā)生。instantiateItem()在每次 ViewPager 需要一個用以顯示的 Object 的時候钞诡,該函數(shù)都會被 ViewPager.addNewItem() 調(diào)用郑现。
notifyDataSetChanged()在數(shù)據(jù)集發(fā)生變化的時候,一般 Activity 會調(diào)用 PagerAdapter.notifyDataSetChanged()荧降,以通知 PagerAdapter接箫,而 PagerAdapter 則會通知在自己這里注冊過的所有 DataSetObserver。其中之一就是在 ViewPager.setAdapter() 中注冊過的 PageObserver朵诫。PageObserver 則進而調(diào)用 ViewPager.dataSetChanged()辛友,從而導致 ViewPager 開始觸發(fā)更新其內(nèi)含 View 的操作。
【FragmentPagerAdapter】
FragmentPagerAdapter 繼承自 PagerAdapter剪返。相比通用的 PagerAdapter废累,該類更專注于每一頁均為 Fragment 的情況邓梅。如文檔所述,該類內(nèi)的每一個生成的 Fragment 都將保存在內(nèi)存之中邑滨,因此適用于那些相對靜態(tài)的頁日缨,數(shù)量也比較少的那種;如果需要處理有很多頁掖看,并且數(shù)據(jù)動態(tài)性較大殿遂、占用內(nèi)存較多的情況,應該使用FragmentStatePagerAdapter乙各。FragmentPagerAdapter 重載實現(xiàn)了幾個必須的函數(shù),因此來自 PagerAdapter 的函數(shù)幢竹,我們只需要實現(xiàn) getCount()耳峦,即可。且焕毫,由于 FragmentPagerAdapter.instantiateItem() 的實現(xiàn)中蹲坷,調(diào)用了一個新增的虛函數(shù) getItem(),因此邑飒,我們還至少需要實現(xiàn)一個 getItem()循签。因此,總體上來說疙咸,相對于繼承自 PagerAdapter县匠,更方便一些。
getItem()該類中新增的一個虛函數(shù)撒轮。函數(shù)的目的為生成新的 Fragment 對象乞旦。重載該函數(shù)時需要注意這一點。在需要時题山,該函數(shù)將被 instantiateItem() 所調(diào)用兰粉。
如果需要向 Fragment 對象傳遞相對靜態(tài)的數(shù)據(jù)時,我們一般通過 Fragment.setArguments() 來進行顶瞳,這部分代碼應當放到 getItem()玖姑。它們只會在新生成 Fragment 對象時執(zhí)行一遍。
如果需要在生成 Fragment 對象后慨菱,將數(shù)據(jù)集里面一些動態(tài)的數(shù)據(jù)傳遞給該 Fragment焰络,那么,這部分代碼不適合放到 getItem() 中抡柿。因為當數(shù)據(jù)集發(fā)生變化時舔琅,往往對應的 Fragment 已經(jīng)生成,如果傳遞數(shù)據(jù)部分代碼放到了 getItem() 中洲劣,這部分代碼將不會被調(diào)用备蚓。這也是為什么很多人發(fā)現(xiàn)調(diào)用 PagerAdapter.notifyDataSetChanged() 后课蔬,getItem() 沒有被調(diào)用的一個原因。instantiateItem()函數(shù)中判斷一下要生成的 Fragment 是否已經(jīng)生成過了郊尝,如果生成過了二跋,就使用舊的,舊的將被 Fragment.attach()流昏;如果沒有扎即,就調(diào)用 getItem() 生成一個新的,新的對象將被 FragmentTransation.add()况凉。
FragmentPagerAdapter 會將所有生成的 Fragment 對象通過 FragmentManager 保存起來備用谚鄙,以后需要該 Fragment 時,都會從 FragmentManager 讀取刁绒,而不會再次調(diào)用 getItem() 方法闷营。
如果需要在生成 Fragment 對象后,將數(shù)據(jù)集中的一些數(shù)據(jù)傳遞給該 Fragment知市,這部分代碼應該放到這個函數(shù)的重載里傻盟。在我們繼承的子類中,重載該函數(shù)嫂丙,并調(diào)用 FragmentPagerAdapter.instantiateItem() 取得該函數(shù)返回 Fragment 對象娘赴,然后,我們該 Fragment 對象中對應的方法跟啤,將數(shù)據(jù)傳遞過去诽表,然后返回該對象。
否則腥光,如果將這部分傳遞數(shù)據(jù)的代碼放到 getItem()中关顷,在 PagerAdapter.notifyDataSetChanged() 后,這部分數(shù)據(jù)設置代碼將不會被調(diào)用武福。destroyItem()該函數(shù)被調(diào)用后议双,會對 Fragment 進行 FragmentTransaction.detach()。這里不是 remove()捉片,只是 detach()平痰,因此 Fragment 還在 FragmentManager 管理中,F(xiàn)ragment 所占用的資源不會被釋放伍纫。
【FragmentStatePagerAdapter】
FragmentStatePagerAdapter 和前面的 FragmentPagerAdapter 一樣宗雇,是繼承子 PagerAdapter。但是莹规,和 FragmentPagerAdapter 不一樣的是赔蒲,正如其類名中的 'State' 所表明的含義一樣,該 PagerAdapter 的實現(xiàn)將只保留當前頁面,當頁面離開視線后舞虱,就會被消除欢际,釋放其資源;而在頁面需要顯示時矾兜,生成新的頁面(就像 ListView 的實現(xiàn)一樣)损趋。這么實現(xiàn)的好處就是當擁有大量的頁面時,不必在內(nèi)存中占用大量的內(nèi)存椅寺。
getItem()一個該類中新增的虛函數(shù)浑槽。
函數(shù)的目的為生成新的 Fragment 對象。
Fragment.setArguments() 這種只會在新建 Fragment 時執(zhí)行一次的參數(shù)傳遞代碼返帕,可以放在這里桐玻。
由于 FragmentStatePagerAdapter.instantiateItem() 在大多數(shù)情況下,都將調(diào)用 getItem() 來生成新的對象荆萤,因此如果在該函數(shù)中放置與數(shù)據(jù)集相關的 setter 代碼畸冲,基本上都可以在 instantiateItem() 被調(diào)用時執(zhí)行,但這和設計意圖不符观腊。畢竟還有部分可能是不會調(diào)用 getItem() 的。因此這部分代碼應該放到 instantiateItem() 中算行。instantiateItem()除非碰到 FragmentManager 剛好從 SavedState 中恢復了對應的 Fragment 的情況外梧油,該函數(shù)將會調(diào)用 getItem() 函數(shù),生成新的 Fragment 對象州邢。新的對象將被 FragmentTransaction.add()儡陨。
FragmentStatePagerAdapter 就是通過這種方式,每次都創(chuàng)建一個新的 Fragment量淌,而在不用后就立刻釋放其資源骗村,來達到節(jié)省內(nèi)存占用的目的的。destroyItem()將 Fragment 移除呀枢,即調(diào)用 FragmentTransaction.remove()胚股,并釋放其資源。
討論
之前看到一些解決辦法裙秋,有的認為這是一個 bug琅拌,應該被修復;有的建議不用 FragmentPagerAdapter摘刑,而改用 FragmentStatePagerAdapter进宝,并且重載 getItemPosition() 并返回 POSITION_NONE,以觸發(fā)銷毀對象以及重建對象枷恕。從上面的分析中看党晋,后者給出的建議確實可以達到調(diào)用 notifyDataSetChanged() 后,F(xiàn)ragment 被以新的參數(shù)重新建立的效果。但是問題在于未玻,如果我們只能這么解決這個問題灾而,豈不是 FragmentPagerAdapter 就用不上了?最關鍵的是深胳,二者對應的情況不同绰疤。對于頁面相對較少的情況,我仍舊希望能夠?qū)⑸傻?Fragment 保存在內(nèi)存中舞终,在需要顯示的時候直接調(diào)用轻庆,而不要產(chǎn)生生成、銷毀對象的額外的開銷敛劝,這樣效率更高余爆。這種情況下,選擇 FragmentPagerAdapter 是更適合夸盟,不加考慮的選擇 FragmentStatePagerAdapter 是不合適的蛾方。我們不能夠因噎廢食。因此上陕,對于 FragmentPagerAdapter 的解決方案就是桩砰,分別重載 getItem() 以及 instantiateItem() 對象。getItem() 只用于生成新的與數(shù)據(jù)無關的 Fragment释簿;而 instantiateItem() 函數(shù)則先調(diào)用父類中的 instantiateItem() 取得所對應的 Fragment 對象亚隅,然后,根據(jù)對應的數(shù)據(jù)庶溶,調(diào)用該對象對應的方法進行數(shù)據(jù)設置煮纵。當然,不要忘記重載 getItemPosition() 函數(shù)偏螺,返回 POSITION_NONE行疏,這個兩個類的解決方案都需要的。二者不同之處在于套像,F(xiàn)ragmentStatePagerAdapter 在會在因 POSITION_NONE 觸發(fā)調(diào)用的 destroyItem() 中真正的釋放資源酿联,重新建立一個新的 Fragment;而 FragmentPagerAdapter 僅僅會在 destroyItem() 中 detach 這個 Fragment夺巩,在 instantiateItem() 時會使用舊的 Fragment货葬,并觸發(fā) attach,因此沒有釋放資源及重建的過程劲够。這樣震桶,當 notifyDataSetChanged() 被調(diào)用后,會最終觸發(fā) instantiateItem()征绎,而不管 getItem() 是否被調(diào)用蹲姐,我們都在重載的 instantiateItem() 函數(shù)中已經(jīng)將所需要的數(shù)據(jù)傳遞給了相應的 Fragment磨取。在 Fragment 接下來的 onCreateView(), onStart() 以及 onResume() 的事件中,它可以正確的讀取新的數(shù)據(jù)柴墩,F(xiàn)ragment 被成功復用了忙厌。這里需要注意一個問題,在 Fragment 沒有被添加到 FragmentManager 之前江咳,我們可以通過 Fragment.setArguments() 來設置參數(shù)逢净,并在 Fragment 中,使用 getArguments() 來取得參數(shù)歼指。這是常用的參數(shù)傳遞方式爹土。但是這種方式對于我們說的情況不適用。因為這種數(shù)據(jù)傳遞方式只可能用一次踩身,在 Fragment 被添加到 FragmentManager 后胀茵,一旦被使用,我們再次調(diào)用 setArguments() 將會導致 java.lang.IllegalStateException: Fragment already active 異常挟阻。因此琼娘,我們這里的參數(shù)傳遞方式選擇是,在繼承的 Fragment 子類中附鸽,新增幾個 setter脱拼,然后通過這些 setter 將數(shù)據(jù)傳遞過去。反向也是類似坷备。相關信息可以參考 [5]挪拟。哦,這些 setter 中要注意不要操作那些 View击你,這些 View 只有在 onCreateView() 事件后才可以操作。針對 FragmentPagerAdapter 的解決辦法如下列代碼所示:</small>
public Fragment getItem(int position) {
MyFragment f = new MyFragment();
return f;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
MyFragment f = (MyFragment) super.instantiateItem(container, position);
String title = mList.get(position);
f.setTitle(title);
return f;
}
@Override
public int getItemPosition(Object object) {
return PagerAdapter.POSITION_NONE;
}