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

本篇文章已受權微信公衆號 dasu_Android(大蘇)獨家發佈html

使用前需知

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

效果

老規矩,先來看看效果圖
github

沒錯,我又入坑了,又從新作了個 Gank 客戶端,由於以前那個代碼寫得太爛了,此次有好好的考慮了下架構之類的事,代碼應該會更容易讀懂了點了,吧。哈哈,再次歡迎來 star 交流哈。緩存

上面的截圖裏有註釋解析了,稍微認真點看看 log 的內容哈,看看是否是你須要的需求。服務器

Fragment懶加載

若是想直接看代碼,直接跳到最下面的代碼部分和使用介紹便可,若是感興趣,能夠慢慢往下看看個人嘮叨。微信

以前寫過一篇 Fragment懶加載和ViewPager的坑,裏面分析了 Fragment 結合 ViewPager 使用時會碰到的一些狀況,以及爲何要用懶加載,如何用,感興趣的也能夠再回去看看。架構

後來發現,我在那篇博客裏封裝的 Fragment 基類不足以知足你們的懶加載需求,因此決定從新來封裝一次,此次封裝的支持如下的功能:ide

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基類,封裝了懶加載的實現
 *
 * 一、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方面的小知識,感興趣的能夠點一波關注,謝謝支持~~

相關文章
相關標籤/搜索