開發中,Fragment 最常見的兩種使用方式就是 ViewPager 巢狀 Fragment ,以及直接通過FragmentManager 來管理 Fragment,對應的互動場景相信大家心裡都有一個原型,沒有的話也沒關係,後邊會有例子的。但這和懶載入有什麼關係呢?試想一下,如果每個 Fragment 都有預設的網路請求操作(也可能是其它耗時操作,這裡以網路請求為例),那麼多個在 Fragment建立過程中都會執行預設網路請求,無論 Fragment 是否對使用者可見,顯然有些浪費流量、影響性 App 效能、使用者體驗不佳等缺點,這些自然不是我們想看到的,出於這些原因,讓 Fragment 進行資料懶載入就有必要了。
先解釋下為什麼會出現多個 Fragment中的預設網路請求都會被執行,由於Fragment在建立的整個過程會走完從onAttach()
到onResume()
的生命週期方法,然而一般情況我們無非在這裡幾個生命週期方法(例如 onActivityCreated()
)裡發起預設的網路請求,所以問題的原因顯而易見,既然不能在 Fragment 生命週期方法直接請求資料,所以就要另謀它法。
我們要做的事情就是讓 Fragment 按需載入資料,即對使用者可見時再請求資料,讓資料的請求時機可控,而不是在初始化建立過程中直接請求資料,同時不受巢狀層級的影響!
接下來我們結合文章開頭提到的兩種 Fragment 使用方式來實現 Fragment 懶載入的功能。
一、ViewPager 巢狀 Fragment
Fragment 有一個非生命週期的setUserVisibleHint(boolean isVisibleToUser)
回撥方法,當 ViewPager 巢狀 Fragment 時會起作用,如果切換 ViewPager 則該方法也會被呼叫,引數isVisibleToUser
為true
代表當前 Fragment 對使用者可見,否則不可見。
目測可以在這個方法中來判斷是否請求資料,但在 Fragment 建立期間setUserVisibleHint()
方法是在onActivityCreated()
之前被呼叫,為了避免不必要的異常,所以在載入請求資料前需要做一個判斷,就是相關的 UI 介面已經建立完畢、並且當前 Fragment 對使用者可見,而且沒請求過資料。先定義一個LazyLoadFragment
基類,繼承自 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 的介面,測試效果如下:
1、這裡我們約定用 tab 標籤上的編號指代對應的 Fragment,例如1-1代表最外層 ViewPager 的第一個 Fragment。 2、ViewPager 都設定
setOffscreenPageLimit()
為其包含的 Fragment 個數
再次執行,只有1-1對使用者可見,按照預期應該只有1-1請求了資料,但是2-1、3-1也請求了資料:
所以問題來了,雖然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();
}
}
}
複製程式碼
再次測試效果如下:
效果符合預期,由於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();
}
}
}
複製程式碼
實際的測試效果如下:
上邊我們用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