寫在前面
Fragment的懶加載,在我看來就是為了抵抗ViewPager的預加載機制所做的抵抗。這樣會使得用戶不需要在一開始就加載好之后的內容,可以節約資源,也可以避免網絡堵塞,增強用戶體驗。
一、名詞解釋
ViewPager為了讓滑動的時候可以有很好的用戶的體驗,也就是防止出現卡頓現象,因此它有一個緩存機制。默認情況下,ViewPager會提前創建好當前Fragment旁的兩個Fragment,舉個例子說也就是如果你當前顯示的是編號3的Fragment,那么其實編號2和4的Fragment也已經創建好了,也就是說這3個Fragment都已經執行完 onAttach() -> onResume() 這之間的生命周期函數了。
什么是懶加載,就是只用到了該用它的時候它才加載。只有當Fragment被切換到了當前頁面的時候,才讓它去請求數據。
二、實現具體思路
在這個懶加載需求的面前,我們很容易就想到,如果有一個方法,他可以做到在Fragment呈現到我們面前的時候才會去加載數據的話,那么就可以直接完成這個需求。在我不懈的百度之下,我發現了下面這個方法。
setUserVisibleHint(boolean isVisibleToUser)
傳聞說,只要將加載數據的操作放到這個里面就可以實現我們的需求,我們來試試。結果如下圖所示:
這里0MainClidFragment卻先打出了false,然后才打出true,這是因為setUserVisibleHint()在Fragment實例化時會先調用一次,并且默認值是false,當選中當前顯示的Fragment時還會再調用一次。
預加載會使0和1位置的Fragment加載到ViewPager中去;而這個方法可以使當前顯示到用戶面前的時候才會顯示為true;那么按理來說就已經基本實現了我們所想要的懶加載了。如果只是為了實現數據加載的話,現在就已經實現了。
但在實際開發過程中,實際上我們還需要進行一些控件的操作,大概是:
1.在Fragment可見時顯示控件,例如:顯示加載控件;
2.在Fragment從可見到不可見時取消控件的顯示,例如:取消加載控件的顯示;
這里說明一下原因;為什么在Fragment可見時顯示控件通過這個方法不一定能成功呢?因為setUserVisibleHint()可能會在Fragment的生命周期之外被調用,也就是可能在view創建前就被調用,也可能在destroyView后被調用,所以如果涉及到一些控件的操作的話,可能會報 null 異常,因為控件還沒初始化,或者已經摧毀了。
所以,我繼續在網上找到了一種關于懶加載的寫法,該題主先后改過兩次,我們直接來看他完成之后的一個版本。他在封裝好一個懶加載的基類之后具體實現了下面幾個功能。
一.支持數據的懶加載且只加載一次;
這一點是常用的一點,我們不能在onCreate()或者onCreateView方法中直接下載數據,因為這樣就會直接根據ViewPager的預加載處理機制來進行處理,就不會有懶加載的效果;而且也必須考慮是不是第一次進入Fragment頁面,如果是第一次進入這個頁面,那么我們就加載數據,如果不是第一次,就呈現上次加載的數據;
二.只有兩種情況會觸發該函數(支持你在這里進行一些 ui 操作,如顯示/隱藏加載框):
1、一種是Fragment從“不可見 -> 可見” 時觸發,并傳入 isVisible = true
2、一種是Fragment從“可見 -> 不可見” 時觸發,并傳入 isVisible = false
因為我們之前說過,setUserVisibleHint()方法很可能在onCreateView方法之前或者onDestroyView之后調用,這個時候我們還沒有進行控件View的初始化或者已經銷毀控件。所以,我們必須進行一些判斷,確保控件已經創建完成且沒有銷毀。
并且,我們需要給出加載控件的顯示和取消就需要把加載控件的呈現與否放置到Fragment從可見到不可見的判斷中去,且需要在ui控件已經創建成功之后觸發。這樣才能對ui進行操作。
三.支持 view 的復用,防止與 ViewPager 使用時出現重復創建 view 的問題。
下面就是該題主封裝之后的BaseFragment代碼:
/**
* Created by dasu on 2016/9/27.
*
* Fragment基類,封裝了懶加載的實現
*
* 1、Viewpager + Fragment情況下,fragment的生命周期因Viewpager的緩存機制而失去了具體意義
* 該抽象類自定義新的回調方法,當fragment可見狀態改變時會觸發的回調方法,和 Fragment 第一次可見時會回調的方法
*
* @see #onFragmentVisibleChange(boolean)
* @see #onFragmentFirstVisible()
*/
public abstract class BaseFragment extends Fragment {
private static final String TAG = BaseFragment.class.getSimpleName();
private boolean isFragmentVisible;
private boolean isReuseView;
private boolean isFirstVisible;
private View rootView;
//setUserVisibleHint()在Fragment創建時會先被調用一次,傳入isVisibleToUser = false
//如果當前Fragment可見,那么setUserVisibleHint()會再次被調用一次,傳入isVisibleToUser = true
//如果Fragment從可見->不可見,那么setUserVisibleHint()也會被調用,傳入isVisibleToUser = false
//總結:setUserVisibleHint()除了Fragment的可見狀態發生變化時會被回調外,在new Fragment()時也會被回調
//如果我們需要在 Fragment 可見與不可見時干點事,用這個的話就會有多余的回調了,那么就需要重新封裝一個
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
//setUserVisibleHint()有可能在fragment的生命周期外被調用
if (rootView == null) {
return;
}
if (isFirstVisible && isVisibleToUser) {
onFragmentFirstVisible();
isFirstVisible = false;
}
if (isVisibleToUser) {
onFragmentVisibleChange(true);
isFragmentVisible = true;
return;
}
if (isFragmentVisible) {
isFragmentVisible = false;
onFragmentVisibleChange(false);
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initVariable();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
//如果setUserVisibleHint()在rootView創建前調用時,那么
//就等到rootView創建完后才回調onFragmentVisibleChange(true)
//保證onFragmentVisibleChange()的回調發生在rootView創建完成之后,以便支持ui操作
if (rootView == null) {
rootView = view;
if (getUserVisibleHint()) {
if (isFirstVisible) {
onFragmentFirstVisible();
isFirstVisible = false;
}
onFragmentVisibleChange(true);
isFragmentVisible = true;
}
}
super.onViewCreated(isReuseView ? rootView : view, savedInstanceState);
}
@Override
public void onDestroyView() {
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
initVariable();
}
private void initVariable() {
isFirstVisible = true;
isFragmentVisible = false;
rootView = null;
isReuseView = true;
}
/**
* 設置是否使用 view 的復用,默認開啟
* view 的復用是指,ViewPager 在銷毀和重建 Fragment 時會不斷調用 onCreateView() -> onDestroyView()
* 之間的生命函數,這樣可能會出現重復創建 view 的情況,導致界面上顯示多個相同的 Fragment
* view 的復用其實就是指保存第一次創建的 view,后面再 onCreateView() 時直接返回第一次創建的 view
*
* @param isReuse
*/
protected void reuseView(boolean isReuse) {
isReuseView = isReuse;
}
/**
* 去除setUserVisibleHint()多余的回調場景,保證只有當fragment可見狀態發生變化時才回調
* 回調時機在view創建完后,所以支持ui操作,解決在setUserVisibleHint()里進行ui操作有可能報null異常的問題
*
* 可在該回調方法里進行一些ui顯示與隱藏,比如加載框的顯示和隱藏
*
* @param isVisible true 不可見 -> 可見
* false 可見 -> 不可見
*/
protected void onFragmentVisibleChange(boolean isVisible) {
}
/**
* 在fragment首次可見時回調,可在這里進行加載數據,保證只在第一次打開Fragment時才會加載數據,
* 這樣就可以防止每次進入都重復加載數據
* 該方法會在 onFragmentVisibleChange() 之前調用,所以第一次打開時,可以用一個全局變量表示數據下載狀態,
* 然后在該方法內將狀態設置為下載狀態,接著去執行下載的任務
* 最后在 onFragmentVisibleChange() 里根據數據下載狀態來控制下載進度ui控件的顯示與隱藏
*/
protected void onFragmentFirstVisible() {
}
protected boolean isFragmentVisible() {
return isFragmentVisible;
}
}
對于上述BaseFragment的代碼思路,我進行思路梳理:
一.為了實現Fragment中的onCreateView中創建的View復用的目的,在這段代碼中使用了onViewCreated,這個是在onCreateView之后觸發的事件,onCreateView的返回值傳入了onViewCreated,就像最后注意事項中說的那樣,如果要完全解決掉ViewPager的View復用問題,就必須在ViewPager的中 destroyItem() 方法,將 super 去掉,也就是不銷毀 view。
二、我們來總結一下setUserVisibleHint()方法的觸發情況:
1.在Fragment創建的時候會調用第一次,isVisibleToUser = false;
2.在fragment從不可見到可見的時候會觸發第二次,isVisibleToUser = true;
3.在Fragment從可見到不可見的時候會觸發第三次,isVisibleToUser = false;
在上述這種情況下,我們要保留第二,第三種情況,所以我們使用rootView==null來進行判斷,為真時用return結束當前方法。直接排除出第一種情況。兩種情況保留成功。
三、現在我們來實現最后一個功能,就是將第一次進入Fragment加載數據和其他情況分開討論。我們在設置一個isFirstVisible的boolean值,用來判斷是否是第一次進入Fragment;如果是的話,實現onFragmentFirstVisible()方法;如果不是的話,實現onFragmentVisibleChange()方法;
最后,先解釋一下BaseFragment中的幾個設置的boolean值;
1.isFragmentVisible:fragment可見;true為可見;
2.isReuseView;View重用;
3.isFirstVisible;是第一次可見;
4.onFragmentVisibleChange(boolean isVisible)
有了這三個boolean 值和onFragmentVisibleChange(boolean isVisible) 方法的幫助,就可以實現對不同情況的梳理:
第一次可見,就是isFirstVisible為真時,調用onFragmentFirstVisible()進行數據加載;
從不可見到可見時,此時isVisibleToUser為真,進行不可見到可見的操作,調用onFragmentVisibleChange(true) 【傳入其中的為true;】即可在使用的時候,實現從不可見到可見過程中的ui操作在這種情況下完成,只需要判斷isVisible為真即可;并在此將isFragmentVisible的值設為true;
而isFragmentVisible是用來判斷Fragment從可見到不可見的標志;因為setUserVisibleHint在這種情況下調用的時候使用這個來判斷是最恰當的,因為此時,isVisibleToUser = false,且isVisibleToUser 不止在這一種情況下等于false,所以不能直接用isVisibleToUser = false來進行判斷。在isFragmentVisible為真的情況下,將isFragmentVisible設為false,并調用onFragmentVisibleChange(false);在這個方法中實現從可見到不可見的ui操作;
使用方法:
使用很簡單,新建你需要的 Fragment 類繼承自該 BaseFragment,然后重寫兩個回調方法,根據你的需要在回調方法里進行相應的操作比如下載數據等即可。
例如:
public class CategoryFragment extends BaseFragment {
private static final String TAG = CategoryFragment.class.getSimpleName();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_category, container, false);
initView(view);
return view;
}
@Override
protected void onFragmentVisibleChange(boolean isVisible) {
if (isVisible) {
//更新界面數據,如果數據還在下載中,就顯示加載框
//從不可見到可見
notifyDataSetChanged();
if (mRefreshState == STATE_REFRESHING) {
mRefreshListener.onRefreshing();
}
} else {
//關閉加載框
//從可見到不可見
mRefreshListener.onRefreshFinish();
}
}
@Override
protected void onFragmentFirstVisible() {
//去服務器下載數據
mRefreshState = STATE_REFRESHING;
mCategoryController.loadBaseData();
}
}
注意事項
1、如果想要讓 fragment 的布局復用成功,需要重寫 viewpager 的適配器里的 destroyItem() 方法,將 super 去掉,也就是不銷毀 view。
2、如果出現切換回來或不相鄰的Tab切換時導致空白界面的問題,解決方法:在 onCreateView中復用布局 + ViewPager 的適配器中復寫 destroyItem() 方法去掉 super。
參考博客:
1.Android Fragment 生命周期onCreatView、onViewCreated - Sun的專欄 - CSDN博客 https://blog.csdn.net/asdf717/article/details/51383750
2.http://www.cnblogs.com/dasusu/p/6745032.html
再次感謝該博主的思路,謝謝。本篇后半段全是轉載,若看正版,請點擊上述鏈接(參考博客2),謝謝。