TabLayout+ViewPager+Fragment實現懶載入完全解決方案

程式碼都tm飛了發表於2018-04-19

TabLayout+ViewPager+Fragment實現懶載入完全解決方案

開發過程中TabLayout配合ViewPager和Fragment的使用是常用的實現多頁面的方式。但是這種方式存在一些問題:ViewPager會對其中的Fragment進行預載入。也就是說使用者第一次開啟第一個介面的時候,不僅第一個介面會進行載入,其他的介面也會進行介面的預載入。這樣就會帶來介面啟動載入慢,浪費系統資源和使用者流量的不好的體驗。而Fragment的懶載入恰好可以解決這個問題.

首先我們來看看其他App的懶載入實現效果,這裡以Bilibili客戶端為例:


實現思路:

通過Fragment的setUserVisibleHint方法,這個方法會傳遞一個boolean型別的引數isVisibleToUser,當Fragment的可見性發生變化時回撥這個方法並把是否可見傳入引數。所以我們可以在這個方法中進行懶載入。

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
	super.setUserVisibleHint(isVisibleToUser);
    if (isVisibleToUser) {
        onLazyLoad();//資料載入操作
    }
}

問題1:

setUserVisibleHint方法在切換介面時會多次呼叫,而我們只希望他被呼叫一次,既第一次進入頁面時被呼叫。

解決:

在Fragment裡建立一個boolean型別的 成員變數isFirstLoad,來判斷當前是否為第一次進入介面。並在Fragment初始化的時候將其初始化為true,在setUserVisibleHint方法中進行判斷如果為true就開始載入資料並將其置為false,否則不載入。

private boolean isFirstLoad = true;//初始化變數

@Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isFirstLoad && isVisibleToUser) {
            onLazyLoad();//資料載入操作
            isFirstLoad = false;//改變變數的值
        }
    }

問題2:

setUserVisibleHint是Fragment的可見性變化時回撥的方法,而onCreateView是Fragment建立檢視時回撥的方法。但是我們無法保證setUserVisibleHint的呼叫發生在onCreateView(也就是檢視建立)之後。那麼我們就是在檢視還沒有建立時進行資料載入,而往往資料的載入會對檢視控制元件進行操作,那麼就會造成空指標的異常發生(因為檢視控制元件還沒有初始化)。

解決:

既然我們要保證資料載入發生在檢視建立之後,那麼我們依然可以通過對isFirstLoad這個成員變數的操作來實現。之前我們是在Fragment建立時就初始化isFirstLoad為true,那麼我們這次就可以將他初始化為false然後在onCreateView中將其初始化為true,這樣就能保證檢視建立完成之前不會進行資料載入的操作。

private boolean isFirstLoad = false;//初始化為false

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.XX,container,false);
	//初始化檢視控制元件...

    isFirstLoad = true;//檢視建立完成,將變數置為true

    return view; 
}

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isFirstLoad && isVisibleToUser) {
            onLazyLoad();//資料載入操作
            isFirstLoad = false;
        }
    }

問題3:

setUserVisibleHint這個方法只在Fragment的可見性改變的時候才會被呼叫,而如果按照上面的程式碼第一次進入頁面之後setUserVisibleHint先被呼叫,這時檢視還沒有完成建立,所以資料載入操作不會被呼叫。而之後沒有切換頁面,Fragment的可見性也就不會發生改變了,setUserVisibleHint也就不會被呼叫了,那麼資料載入也就不會被執行了。

解決:

既然要保證在檢視建立完成後要進行一次資料載入,那麼就在onCreateView方法中手動呼叫一次資料載入就好了。不過這裡也要進行對isFirstLoad的操作,在資料載入之前要通過getUserVisibleHint()判斷可見性,在載入後還要將isFirstLoad置為false。這樣才足夠嚴謹。

private boolean isFirstLoad = false;

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.XX,container,false);
	//初始化檢視控制元件...

    isFirstLoad = true;//檢視建立完成,將變數置為true 

    if (getUserVisibleHint()) {//判斷Fragment是否可見
        onLazyLoad();//資料載入操作
        isFirstLoad = false;//將變數置為false
    }
    return view;
}

@Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isFirstLoad && isVisibleToUser) {
            onLazyLoad();//資料載入操作
            isFirstLoad = false;//將變數置為false
        }

    }

完全解決方案:

建立一個抽象父類:BaseLazyLoadFragment,在父類中實現整個懶載入的流程,然後將初始化檢視、初始化事件和資料載入的介面暴露給子類讓子類來實現。完整程式碼:

public abstract class BaseLazyLoadFragment extends Fragment {

    private boolean isFirstLoad = false;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {

        View view = initView(inflater, container);//讓子類實現初始化檢視

        initEvent();//初始化事件

        isFirstLoad = true;//檢視建立完成,將變數置為true

        if (getUserVisibleHint()) {//如果Fragment可見進行資料載入
            onLazyLoad();
            isFirstLoad = false;
        }
        return view;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        isFirstLoad = false;//檢視銷燬將變數置為false
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isFirstLoad && isVisibleToUser) {//檢視變為可見並且是第一次載入
            onLazyLoad();
            isFirstLoad = false;
        }

    }
    //資料載入介面,留給子類實現
    public abstract void onLazyLoad();
    
    //初始化檢視介面,子類必須實現
    public abstract View initView(LayoutInflater inflater, @Nullable ViewGroup container);
	
	//初始化事件介面,留給子類實現
    public abstract void initEvent();

}

使用:

如果當前的Fragment要用到懶載入功能的話,只要讓他繼承自BaseLazyLoadFragment,並重寫對應的方法就可以了。

效果展示:

這裡只有一個Activity,然後通過TabLayout+ViewPager+Fragment來實現多頁面,接著用BaseLazyLoadFragment來實現懶載入。這裡用SwipeRefreshLayout的載入和檢視的隱藏/顯示來模擬資料載入過程。


注意:ViewPager預設只保留當前檢視的前後各一個檢視,其他的檢視會被銷燬。如果不想讓檢視被銷燬要重寫FragmentPagerAdapter的destroyItem方法,並註釋掉原本的程式碼。

總結:

其實之所以Fragment的懶載入這麼麻煩是因為Fragment的生命週期很特殊,Fragment的幾個生命週期方法都是和他所繫結的Activity的生命週期對應的。但是當Activity可見的時候Fragment卻不一定可見,所以出現了Fragment專有的生命週期方法:setUserVisibleHint()。但是在使用時也不可以脫離Activity的那幾個生命週期來使用,所以懶載入實現起來才有這麼多講究。

相關文章