1. 程式人生 > >fragment 懶載入3

fragment 懶載入3

使用前需知

2017-7-14更新: 目前有人使用後出現了諸如首次開啟顯示空白介面,但點選有反應;或來回切換又變空白介面的問題。這些問題我暫時還不知道該怎麼解決,後期有時間時會具體去分析下問題該怎麼解決。所以你如果要使用該程式碼,希望考慮一下,我自己的小應用目前是沒碰到這些問題。

效果

老規矩,先來看看效果圖


演示
log

沒錯,我又入坑了,又重新做了個 Gank 客戶端,因為之前那個程式碼寫得太爛了,這次有好好的考慮了下架構之類的事,程式碼應該會更容易讀懂了點了,吧。哈哈,再次歡迎來 star 交流哈。

上面的截圖裡有註釋解析了,稍微認真點看看 log 的內容哈,看看是不是你需要的需求。

Fragment懶載入

如果想直接看程式碼,直接跳到最下面的程式碼部分和使用介紹即可,如果感興趣,可以慢慢往下看看我的嘮叨。

之前寫過一篇 Fragment懶載入和ViewPager的坑,裡面分析了 Fragment 結合 ViewPager 使用時會碰到的一些情況,以及為什麼要用懶載入,如何用,感興趣的也可以再回去看看。

後來發現,我在那篇部落格裡封裝的 Fragment 基類不足以滿足大家的懶載入需求,所以決定重新來封裝一次,這次封裝的支援以下的功能:

1.支援資料的懶載入並且只加載一次

2.提供 Fragment 可見與不可見時回撥,支援你在這裡進行一些 ui 操作,如顯示/隱藏載入框

3.支援 view 的複用,防止與 ViewPager 使用時出現重複建立 view 的問題

第一點應該是比較需要且常用的一點,之前那篇部落格裡沒有考慮到這點應用場景是我的疏忽。稍微講解一下,有些時候,我們開啟一個 Fragment 頁面時,希望它是在可見時才去載入資料,也就是不要在後臺就開始載入資料,而且,我們也希望載入資料的操作只是第一次開啟該 Fragment 時才進行的操作,以後如果再重新開啟該 Fragment 的話,就不要再重複的去載入資料了。

具體點說,Fragment 和 ViewPager 一起用時,由於 ViewPager 的快取機制,在開啟一個 Fragment 時,它旁邊的幾個 Fragment 其實也已經被建立了,如果我們是在 Fragment 的 onCreat()

或者 onCreateView() 裡去跟伺服器互動,下載介面資料,那麼這時這些已經被建立的 Fragment,就都會出現在後臺下載資料的情況了。所以我們通常需要在 setUserVisibleHint() 裡去判斷當前 Fragment 是否可見,可見時再去下載資料,但是這樣還是會出現一個問題,就是每次可見時都會重複去下載資料,我們希望的是隻有第一次可見時才需要去下載,那麼就還需要再做一些判斷。這就是要封裝個基類來做這些事了,具體程式碼見後面。

即使我們在 setUserVisibleHint() 做了很多判斷,實現了可見時載入並且只有第一次可見時才載入,可能還是會遇到其他問題。比如說,我下載完資料就直接需要對 ui 進行操作,將資料展示出來,但有時卻報了 ui 控制元件 null 異常,這是因為 setUserVisibleHint() 有可能在 onCreateView() 建立 view 之前呼叫,而且資料載入時間很短,這就可能出現 null 異常了,那麼我們還需要再去做些判斷,保證在資料下載完後 ui 控制元件已經建立完成。

除了懶載入,只加載一次的需求外,可能我們還需要每次 Fragment 的開啟或關閉時顯示資料載入進度。對吧,我們開啟一個 Fragment 時,如果資料還沒下載完,那麼應該給個下載進度或者載入框提示,如果這個時候打開了新的 Fragment 頁面,然後又重新返回時,如果資料還沒載入完,那麼也還應該繼續給提示,對吧。這就需要有個 Fragment 可見與不可見時觸發的回撥方法,並且該方法還得保證是在 view 建立完後才觸發的,這樣才能支援對 ui 進行操作。

以上,就是我們封裝的 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;
    }
}

使用方法

使用很簡單,新建你需要的 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。

最後,繼續不要臉的貼上我最近在做的 Gank 客戶端的專案地址啦,專案沒引入什麼高階的庫,都是用的最基本的程式碼實現的,專案也按模組來劃分,也儘可能的實現ui和邏輯的劃分,各模組也嚴格控制權限,儘量讓模組之間,類之間的耦合減少些,之所以這樣是為了後面更深入理解mvp做準備,總之,程式碼應該還是很容易可以看懂的吧,歡迎大家star交流。


GanHuo