Android Fragment 懶載入實踐
開發中,Fragment 最常見的兩種使用方式就是 ViewPager 巢狀 Fragment ,以及直接通過FragmentManager 來管理 Fragment,對應的互動場景相信大家心裡都有一個原型,沒有的話也沒關係,後邊會有例子的。但這和懶載入有什麼關係呢?試想一下,如果每個 Fragment 都有預設的網路請求操作(也可能是其它耗時操作,這裡以網路請求為例),那麼多個在 Fragment建立過程中都會執行預設網路請求,無論 Fragment 是否對使用者可見,顯然有些浪費流量、影響性 App 效能、使用者體驗不佳等缺點,這些自然不是我們想看到的,出於這些原因,讓 Fragment 進行資料懶載入就有必要了。

Picture
先解釋下為什麼會出現多個 Fragment中的預設網路請求都會被執行,由於Fragment在建立的整個過程會走完從 onAttach()
到 onResume()
的生命週期方法,然而一般情況我們無非在這裡幾個生命週期方法(例如 onActivityCreated()
)裡發起預設的網路請求,所以問題的原因顯而易見,既然不能在 Fragment 生命週期方法直接請求資料,所以就要另謀它法。
我們要做的事情就是讓 Fragment 按需載入資料,即對使用者可見時再請求資料,讓資料的請求時機可控,而不是在初始化建立過程中直接請求資料,同時不受巢狀層級的影響!
接下來我們結合文章開頭提到的兩種 Fragment 使用方式來實現 Fragment 懶載入的功能。
一、ViewPager 巢狀 Fragment
Fragment 有一個非生命週期的 setUserVisibleHint(boolean isVisibleToUser)
回撥方法,當 ViewPager 巢狀 Fragment 時會起作用,如果切換 ViewPager 則該方法也會被呼叫,引數 isVisibleToUser
為 true
代表當前 Fragment 對使用者可見,否則不可見。
目測可以在這個方法中來判斷是否請求資料,但在 Fragment 建立期間```setUserVisibleHint BaseFragment ,程式碼如下:
public abstract class LazyLoadFragment extends BaseFragment { private boolean isViewCreated; // 介面是否已建立完成 private boolean isVisibleToUser; // 是否對使用者可見 private boolean isDataLoaded; // 資料是否已請求 // 實現具體的資料請求邏輯 protected abstract void loadData(); @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); this.isVisibleToUser = isVisibleToUser; tryLoadData(); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); isViewCreated = true; tryLoadData(); } public void tryLoadData() { if (isViewCreated && isVisibleToUser && !isDataLoaded) { loadData(); isDataLoaded = true; } } }
ViewPager 中第一個可見的 Fragment 會走 onActivityCreated()
方法去請求資料,之後切換 Fragment 會走 setUserVisibleHint()
方法去嘗試請求資料。這樣我們的 Fragment 繼承 LazyLoadFragment,然後實現 loadData()
方法去完成資料的請求即可,寫一個簡單的 ViewPager 巢狀 Fragment 的介面,測試效果如下:

Picture
一切正常,符合我們的預期,由於現在只是一層巢狀,我們再加兩層 ViewPager 巢狀 Fragment 試試,如下圖(具體的實現可參考原始碼):

Picture
1、這裡我們約定用 tab 標籤上的編號指代對應的 Fragment,例如 1-1 代表最外層 ViewPager 的第一個 Fragment。
2、ViewPager 都設定 setOffscreenPageLimit()
為其包含的 Fragment 個數
再次執行,只有 1-1 對使用者可見,按照預期應該只有 1-1 請求了資料,但是 2-1 、 3-1 也請求了資料:

Picture
所以問題來了,雖然 2-1 、 3-1 對使用者不可見,但在建立過程中它們的 setUserVisibleHint()
的 isVisibleToUser
引數最終為 true
,從而在 onActivityCreated()
方法中請求了資料。注意此時 2-1 、 3-1 的父 Fragment 也是不可見的,所以要解決這個問題,可以在 tryLoadData()
方法中判斷當前要請求資料的 Fragment 的 父 Fragment 是否可見,不可見則不請求資料。
但新的問題又來了,這個導致該 Fragment 失去了初次請求資料的機會,即便該 Fragment 初次對使用者可見時也不會主動去請求資料,需要來回再切換一次才會請求資料,要解決這個問題,可以讓該 Fragment 的父 Fragment 請求資料時通知子 Fragment 去請求資料,修改下程式碼:
public abstract class LazyLoadFragment extends BaseFragment { private boolean isViewCreated; // 介面是否已建立完成 private boolean isVisibleToUser; // 是否對使用者可見 private boolean isDataLoaded; // 資料是否已請求, isNeedReload()返回false的時起作用 // 實現具體的資料請求邏輯 protected abstract void loadData(); @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); this.isVisibleToUser = isVisibleToUser; tryLoadData(); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); isViewCreated = true; tryLoadData(); } /** * ViewPager場景下,判斷父fragment是否可見 * * @return */ private boolean isParentVisible() { Fragment fragment = getParentFragment(); return fragment == null || (fragment instanceof LazyLoadFragment && ((LazyLoadFragment) fragment).isVisibleToUser); } /** * ViewPager場景下,當前fragment可見,如果其子fragment也可見,則嘗試讓子fragment請求資料 */ private void dispatchParentVisibleState() { FragmentManager fragmentManager = getChildFragmentManager(); List<Fragment> fragments = fragmentManager.getFragments(); if (fragments.isEmpty()) { return; } for (Fragment child : fragments) { if (child instanceof LazyLoadFragment && ((LazyLoadFragment) child).isVisibleToUser) { ((LazyLoadFragment) child).tryLoadData(); } } } public void tryLoadData() { if (isViewCreated && isVisibleToUser && isParentVisible() &&!isDataLoaded) { loadData(); isDataLoaded = true; // 通知 子 Fragment 請求資料 dispatchParentVisibleState(); } } }
再次測試效果如下:

Picture
效果符合預期,由於 1-2 、 2-1 同時可見,所以會幾乎同時請求資料, 2-2 、 3-1 也類似。
至此 ViewPager 巢狀 Fragment 形式的懶載入就實現了。
二、FragmentManager 管理 Fragment
FragmentManager 管理 Fragment 時,和 ViewPager 巢狀 Fragment 中的問題類似,但此時 setUserVisibleHint()
方法並不會被呼叫,所以要尋找新的途徑了。
當用 FragmentManager 來 add()
、 hide()
、 show()
Fragment 時 Fragment 的 onHiddenChanged(boolean hidden)
方法會被呼叫,其中 hidden
引數為 false
時代表對應 Fragment 可見,否則不可見,注意三個操作裡當執行 show()
操作時 hidden
引數才為 false
,同時由於該方法在 onActivityCreated()
之後被呼叫。我們可以直接在 onHiddenChanged()
方法引數為 false
時發起資料請求即可。
當存在多層巢狀的情況時,即 FragmentManager 管理的 Fragment 內部又使用 FragmentManager 管理新的 Fragment,這種情況和多層 ViewPager 巢狀 Fragment 時的處理方法類似,即判斷當前 Fragment 的父 Fragment 是否可見、以及 Fragment 可見時通知子 Fragment 去請求資料。
主要的問題就這些,看下程式碼實現:
public abstract class LazyLoadFragment extends BaseFragment { private boolean isDataLoaded; // 資料是否已請求 private boolean isHidden = true; // 記錄當前fragment的是否隱藏 // 實現具體的資料請求邏輯 protected abstract void loadData(); /** * 使用show()、hide()控制fragment顯示、隱藏時回撥該方法 * * @param hidden */ @Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); isHidden = hidden; if (!hidden) { tryLoadData1(); } } /** * show()、hide()場景下,當前fragment沒隱藏,如果其子fragment也沒隱藏,則嘗試讓子fragment請求資料 */ private void dispatchParentHiddenState() { FragmentManager fragmentManager = getChildFragmentManager(); List<Fragment> fragments = fragmentManager.getFragments(); if (fragments.isEmpty()) { return; } for (Fragment child : fragments) { if (child instanceof LazyLoadFragment && !((LazyLoadFragment) child).isHidden) { ((LazyLoadFragment) child).tryLoadData1(); } } } /** * show()、hide()場景下,父fragment是否隱藏 * * @return */ private boolean isParentHidden() { Fragment fragment = getParentFragment(); if (fragment == null) { return false; } else if (fragment instanceof LazyLoadFragment && !((LazyLoadFragment) fragment).isHidden) { return false; } return true; } /** * show()、hide()場景下,嘗試請求資料 */ public void tryLoadData1() { if (!isParentHidden() && !isDataLoaded) { loadData(); isDataLoaded = true; dispatchParentHiddenState(); } } }
實際的測試效果如下:

Picture
上邊我們用 isDataLoaded
控制 Fragment 只請求一次資料,如果需要每次 Fragment 可見都請求資料,我們只需對 LazyLoadFragment
做如下修改:
public abstract class LazyLoadFragment extends BaseFragment { /** * fragment再次可見時,是否重新請求資料,預設為flase則只請求一次資料 * * @return */ protected boolean isNeedReload() { return false; } /** * ViewPager場景下,嘗試請求資料 */ public void tryLoadData() { if (isViewCreated && isVisibleToUser && isParentVisible() && (isNeedReload() || !isDataLoaded)) { loadData(); isDataLoaded = true; dispatchParentVisibleState(); } } /** * show()、hide()場景下,嘗試請求資料 */ public void tryLoadData1() { if (!isParentHidden() && (isNeedReload() || !isDataLoaded)) { loadData(); isDataLoaded = true; dispatchParentHiddenState(); } } }
添加了一個 isNeedReload()
方法,如果子類需要每次可見都請求資料,重寫該方法返回 true
即可。
1、對於Activity, getSupportFragmentManager()
得到的是FragmentActivity的FragmentManager物件
2、對於Fragment, getFragmentManager()
得到的是父Fragment的FragmentManager物件,如果沒有父Fragment,則是FragmentActivity的FragmentManager物件
3、 getChildFragmentManager()
得到是Fragment自身的FragmentManager物件
至此, LazyLoadFragment
基類就完成了,只要繼承它就可以輕鬆實現懶載入功能,更多細節戳這裡: LazyLoadFragment

image

image