爲何調用 FragmentPagerAdapter.notifyDataSetChanged() 並不能更新其 Fragment?【轉載】

 

轉載自:http://www.cnblogs.com/dancefire/archive/2013/01/02/why-notifyDataSetChanged-does-not-work.htmlhtml

 

在一個 Android 應用中,我使用 FragmentPagerAdapter 來處理多 Fragment 頁面的橫向滑動。不過我碰到了一個問題,即當 Fragment 對應的數據集發生改變時,我但願可以經過調用 mAdapter.notifyDataSetChanged() 來觸發 Fragment 頁面使用新的數據調整或從新生成其內容,但是當我調用 notifyDataSetChanged() 後,發現什麼都沒發生。java

   

搜索以後發現不止我一我的碰到這個問題,你們給出的解決辦法五花八門,有些確實解決了問題,可是我總感受問題沒搞清楚。因而我決定搞明白這個問題究竟是怎麼回事,以及正確的用法到底如何。要搞明白這個問題,僅僅閱讀文檔並不足夠,還須要閱讀相關幾個類的相關方法的實現,搞懂其設計意圖。下面就是經過閱讀源代碼搞明白的內容。android

   

   

ViewPagerapp

 

ViewPager 如其名所述,是負責翻頁的一個 View。準確說是一個 ViewGroup,包含多個 View 頁,在手指橫向滑動屏幕時,其負責對 View 進行切換。爲了生成這些 View 頁,須要提供一個 PagerAdapter 來進行和數據綁定以及生成最終的 View 頁。ide

   

    • ViewPager 經過 setAdapter() 來創建與 PagerAdapter 的聯繫。這個聯繫是雙向的,一方面,ViewPager 會擁有 PagerAdapter 對象,從而能夠在須要時調用 PagerAdapter 的方法;另外一方面,ViewPager 會在 setAdapter() 調用 PagerAdapter registerDataSetObserver() 方法,註冊一個本身生成的 PagerObserver 對象,從而在 PagerAdapter 有所須要時(如 notifyDataSetChanged()  notifyDataSetInvalidated() 時),能夠調用 Observer onChanged() onInvalidated() 方法,從而實現 PagerAdapter ViewPager 方向發送信息。
    •  PagerObserver.onChanged(),以及 PagerObserver.onInvalide() 中被調用。所以當 PagerAdapter.notifyDataSetChanged() 被觸發時,ViewPager.dataSetChanged() 也能夠被觸發。該函數將使用 getItemPosition() 的返回值來進行判斷,若是爲 POSITION_UNCHANGED,則什麼都不作;若是爲 POSITION_NONE,則調用 PagerAdapter.destroyItem() 來去掉該對象,並設置爲須要刷新 (needPopulate = true) 以便觸發 PagerAdapter.instantiateItem() 來生成新的對象。

   

PagerAdapter函數

   

PageAdapter  ViewPager 的支持者,ViewPager 將調用它來取得所需顯示的頁,而 PageAdapter 也會在數據變化時,通知 ViewPager。這個類也是FragmentPagerAdapter 以及 FragmentStatePagerAdapter 的基類。若是繼承自該類,至少須要實現 instantiateItem(), destroyItem(), getCount() 以及 isViewFromObject()ui

   

    • 該函數用以返回給定對象的位置,給定對象是由 instantiateItem() 的返回值。
    • ViewPager.dataSetChanged() 中將對該函數的返回值進行判斷,以決定是否最終觸發 PagerAdapter.instantiateItem() 函數。
    • PagerAdapter 中的實現是直接傳回 POSITION_UNCHANGED。若是該函數不被重載,則會一直返回 POSITION_UNCHANGED,從而致使 ViewPager.dataSetChanged() 被調用時,認爲沒必要觸發 PagerAdapter.instantiateItem()。不少人由於沒有重載該函數,而致使調用
       PagerAdapter.notifyDataSetChanged()
      後,什麼都沒有發生。
    • 在每次 ViewPager 須要一個用以顯示的 Object 的時候,該函數都會被 ViewPager.addNewItem() 調用。
    • 在數據集發生變化的時候,通常 Activity 會調用 PagerAdapter.notifyDataSetChanged(),以通知 PagerAdapter,而 PagerAdapter 則會通知在本身這裏註冊過的全部 DataSetObserver。其中之一就是在 ViewPager.setAdapter() 中註冊過的 PageObserverPageObserver 則進而調用 ViewPager.dataSetChanged(),從而致使 ViewPager 開始觸發更新其內含 View 的操做。

   

FragmentPagerAdaptergoogle

   

FragmentPagerAdapter 繼承自 PagerAdapter。相比通用的 PagerAdapter,該類更專一於每一頁均爲 Fragment 的狀況。如文檔所述,該類內的每個生成的 Fragment 都將保存在內存之中,所以適用於那些相對靜態的頁,數量也比較少的那種;若是須要處理有不少頁,而且數據動態性較大、佔用內存較多的狀況,應該使用FragmentStatePagerAdapterFragmentPagerAdapter 重載實現了幾個必須的函數,所以來自 PagerAdapter 的函數,咱們只須要實現 getCount(),便可。且,因爲 FragmentPagerAdapter.instantiateItem() 的實現中,調用了一個新增的虛函數 getItem(),所以,咱們還至少須要實現一個 getItem()。所以,整體上來講,相對於繼承自 PagerAdapter,更方便一些。spa

   

    • 該類中新增的一個虛函數。函數的目的爲生成新的 Fragment 對象。重載該函數時須要注意這一點。在須要時,該函數將被 instantiateItem() 所調用。
    • 若是須要向 Fragment 對象傳遞相對靜態的數據時,咱們通常經過 Fragment.setArguments() 來進行,這部分代碼應當放到 getItem()。它們只會在新生成 Fragment 對象時執行一遍。
    • 若是須要在生成 Fragment 對象後,將數據集裏面一些動態的數據傳遞給該 Fragment,那麼,這部分代碼不適合放到 getItem() 中。由於當數據集發生變化時,每每對應的 Fragment 已經生成,若是傳遞數據部分代碼放到了 getItem() 中,這部分代碼將不會被調用。這也是爲何不少人發現調用 PagerAdapter.notifyDataSetChanged() 後,getItem() 沒有被調用的一個緣由。
    • 函數中判斷一下要生成的 Fragment 是否已經生成過了,若是生成過了,就使用舊的,舊的將被 Fragment.attach();若是沒有,就調用 getItem() 生成一個新的新的對象將被 FragmentTransation.add()
    • FragmentPagerAdapter 會將全部生成的 Fragment 對象經過 FragmentManager 保存起來備用,之後須要該 Fragment 時,都會從 FragmentManager 讀取,而不會再次調用 getItem() 方法
    • 若是須要在生成 Fragment 對象後,將數據集中的一些數據傳遞給該 Fragment,這部分代碼應該放到這個函數的重載裏。在咱們繼承的子類中,重載該函數,並調用 FragmentPagerAdapter.instantiateItem() 取得該函數返回 Fragment 對象,而後,咱們該 Fragment 對象中對應的方法,將數據傳遞過去,而後返回該對象。
    • 不然,若是將這部分傳遞數據的代碼放到 getItem()中,在 PagerAdapter.notifyDataSetChanged() 後,這部分數據設置代碼將不會被調用。
    • 該函數被調用後,會對 Fragment 進行 FragmentTransaction.detach()。這裏不是 remove(),只是 detach(),所以 Fragment 還在 FragmentManager 管理中,Fragment 所佔用的資源不會被釋放。

   

FragmentStatePagerAdapter設計

   

   

FragmentStatePagerAdapter 和前面的 FragmentPagerAdapter 同樣,是繼承子 PagerAdapter。可是,和 FragmentPagerAdapter 不同的是,正如其類名中的 'State' 所代表的含義同樣,該 PagerAdapter 的實現將只保留當前頁面,當頁面離開視線後,就會被消除,釋放其資源;而在頁面須要顯示時,生成新的頁面(就像 ListView 的實現同樣)。這麼實現的好處就是當擁有大量的頁面時,沒必要在內存中佔用大量的內存。

   

    • 一個該類中新增的虛函數。
    • 函數的目的爲生成新的 Fragment 對象。
    • Fragment.setArguments() 這種只會在新建 Fragment 時執行一次的參數傳遞代碼,能夠放在這裏。
    • 因爲 FragmentStatePagerAdapter.instantiateItem() 在大多數狀況下,都將調用 getItem() 來生成新的對象,所以若是在該函數中放置與數據集相關的 setter 代碼,基本上均可以在 instantiateItem() 被調用時執行,但這和設計意圖不符。畢竟還有部分可能是不會調用 getItem() 的。所以這部分代碼應該放到 instantiateItem() 中。
    • 除非碰到 FragmentManager 恰好從 SavedState 中恢復了對應的 Fragment 的狀況外,該函數將會調用 getItem() 函數,生成新的 Fragment 對象。新的對象將被 FragmentTransaction.add()
    • FragmentStatePagerAdapter 就是經過這種方式,每次都建立一個新的 Fragment,而在不用後就馬上釋放其資源,來達到節省內存佔用的目的的。

   

討論

   

以前看到一些解決辦法,有的認爲這是一個 bug,應該被修復;有的建議不用 FragmentPagerAdapter,而改用 FragmentStatePagerAdapter,而且重載 getItemPosition() 並返回 POSITION_NONE,以觸發銷燬對象以及重建對象。從上面的分析中看,後者給出的建議確實能夠達到調用 notifyDataSetChanged() 後,Fragment 被以新的參數從新創建的效果。

   

可是問題在於,若是咱們只能這麼解決這個問題,豈不是 FragmentPagerAdapter 就用不上了?最關鍵的是,兩者對應的狀況不一樣。對於頁面相對較少的狀況,我仍舊但願可以將生成的 Fragment 保存在內存中,在須要顯示的時候直接調用,而不要產生生成、銷燬對象的額外的開銷,這樣效率更高。這種狀況下,選擇 FragmentPagerAdapter 是更適合,不加考慮的選擇 FragmentStatePagerAdapter 是不合適的。咱們不可以因噎廢食。

   

所以,對於 FragmentPagerAdapter 的解決方案就是,分別重載 getItem() 以及 instantiateItem() 對象。getItem() 只用於生成新的與數據無關的 Fragment;而 instantiateItem() 函數則先調用父類中的 instantiateItem() 取得所對應的 Fragment 對象,而後,根據對應的數據,調用該對象對應的方法進行數據設置。

   

固然,不要忘記重載 getItemPosition() 函數,返回 POSITION_NONE,這個兩個類的解決方案都須要的。兩者不一樣之處在於,FragmentStatePagerAdapter 在會在因 POSITION_NONE 觸發調用的 destroyItem() 中真正的釋放資源,從新創建一個新的 Fragment;而 FragmentPagerAdapter 僅僅會在 destroyItem() detach 這個 Fragment,在 instantiateItem() 時會使用舊的 Fragment,並觸發 attach,所以沒有釋放資源及重建的過程。

   

這樣,當 notifyDataSetChanged() 被調用後,會最終觸發 instantiateItem(),而無論 getItem() 是否被調用,咱們都在重載的 instantiateItem() 函數中已經將所須要的數據傳遞給了相應的 Fragment。在 Fragment 接下來的 onCreateView(), onStart() 以及 onResume() 的事件中,它能夠正確的讀取新的數據,Fragment 被成功複用了。

   

這裏須要注意一個問題,在 Fragment 沒有被添加到 FragmentManager 以前,咱們能夠經過 Fragment.setArguments() 來設置參數,並在 Fragment 中,使用 getArguments() 來取得參數。這是經常使用的參數傳遞方式。可是這種方式對於咱們說的狀況不適用。由於這種數據傳遞方式只可能用一次,在 Fragment 被添加到 FragmentManager 後,一旦被使用,咱們再次調用 setArguments() 將會致使 java.lang.IllegalStateException: Fragment already active 異常。所以,咱們這裏的參數傳遞方式選擇是,在繼承的 Fragment 子類中,新增幾個 setter,而後經過這些 setter 將數據傳遞過去。反向也是相似。相關信息能夠參考 [5]。哦,這些 setter 中要注意不要操做那些 View,這些 View 只有在 onCreateView() 事件後才能夠操做。

   

針對 FragmentPagerAdapter 的解決辦法以下列代碼所示:

   

   

1 @Override

2 public Fragment getItem(int position) {

3 MyFragment f = new MyFragment();

4 return f;

5 }

6

7 @Override

8 public Object instantiateItem(ViewGroup container, int position) {

9 MyFragment f = (MyFragment) super.instantiateItem(container, position);

10 String title = mList.get(position);

11 f.setTitle(title);

12 return f;

13 }

14

15 @Override

16 public int getItemPosition(Object object) {

17 return PagerAdapter.POSITION_NONE;

18 }

   

   

參考

   

Android 文檔:

[1] http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html

[2] http://developer.android.com/reference/android/support/v4/app/FragmentPagerAdapter.html

[3] http://developer.android.com/reference/android/support/v4/app/FragmentStatePagerAdapter.html

[4] http://developer.android.com/reference/android/support/v4/view/ViewPager.html

[5] http://developer.android.com/guide/components/fragments.html#CommunicatingWithActivity

   

Android 源代碼:

[6] http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/support/v4/view/PagerAdapter.java#PagerAdapter

[7] http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/support/v4/app/FragmentPagerAdapter.java#FragmentPagerAdapter

[8] http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/support/v4/app/FragmentStatePagerAdapter.java#FragmentStatePagerAdapter

[9] http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/support/v4/view/ViewPager.java#ViewPager

   

Android Issue List:

[10] http://code.google.com/p/android/issues/detail?id=19001

相關文章
相關標籤/搜索