【Android】再來一篇Fragment懶加載(只加載一次哦)


本篇文章已授權微信公眾號 dasu_Android(大蘇)獨家發布

使用前需知

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

效果

老規矩,先來看看效果圖

沒錯,我又入坑了,又重新做了個 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:https://github.com/woshidasusu/GanHuo
Meizi:https://github.com/woshidasusu/Meizi


QQ圖片20180316094923.jpg
最近剛開通了公眾號,想激勵自己堅持寫作下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的可以點一波關注,謝謝支持~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM