【Android】Fragment懶加載和ViewPager的坑

本篇文章已授權微信公眾號 安卓巴士Android開發者門戶 獨家發布

效果

老規矩,先來看看效果

效果圖

ANDROID和福利兩個Fragment是設置的Fragment可見時加載數據,也就是懶加載。圓形的旋轉加載圖標只有一個,所以,如果當前Fragment正處于加載狀態,在離開該Fragment時需要隱藏加載動畫,因為另一個Fragment并不一定處于加載狀態,當返回Fragment時,如果還是處于加載狀態,則要可以實現自動顯示加載動畫,如果數據已經加載完畢則不需要再顯示出來。

以上效果就是今天要介紹和分享的,那么開始往下看吧。

懶加載

懶加載意思也就是當需要的時候才會去加載

那么,為什么Fragment需要懶加載呢,一般我們都會在onCreate()或者onCreateView()里去啟動一些數據加載操作,比如從本地加載或者從服務器加載。大部分情況下,這樣并不會出現什么問題,但是當你使用ViewPager + Fragment的時候,問題就來了,這時就應該考慮是否需要實現懶加載了。

ViewPager + Fragment 的坑

ViewPager為了讓滑動的時候可以有很好的用戶的體驗,也就是防止出現卡頓現象,因此它有一個緩存機制。默認情況下,ViewPager會提前創建好當前Fragment旁的兩個Fragment,舉個例子說也就是如果你當前顯示的是編號3的Fragment,那么其實編號2和4的Fragment也已經創建好了,也就是說這3個Fragment都已經執行完 onAttach() -> onResume() 這之間的生命周期函數了。

日志圖1

本來Fragment的 onResume()表示的是當前Fragment處于可見且可交互狀態,但由于ViewPager的緩存機制,它已經失去了意義,也就是說我們只是打開了“福利”這個Fragment,但其實“休息視頻”和“拓展資源”這兩個Fragment的數據也都已經加載好了。

如果加載數據的操作都比較耗時或者都是類似圖片的占用大量內存,這時就應該考慮想想是否該實現懶加載。也就是,當我打開哪個Fragment的時候,它才會去加載數據。

懶加載實現?

setUserVisibleHint(boolean isVisibleToUser) 可以?

當你去網上查找相關資料時,你會發現很多人推薦說把加載數據的操作放在這個函數里,isVisibleToUser表示當前Fragment是否可見。那么,是否真的可以就這樣做呢?先來看個日志:

日志圖2

題主是從 DayDataFragment 跳轉到 MeiziDataFragment 的,所以可以看到日志里面:DayDataFragment打出了false,表示它不可見了。而MeiziDataFragment卻先打出了false,然后才打出true,這是因為setUserVisibleHint()在Fragment實例化時會先調用一次,并且默認值是false,當選中當前顯示的Fragment時還會再調用一次。

所以,看上面的日志,除了DayDataFragment外,其他三個Fragment均沒有實例化,所以當打開MeiziDataFragment時,因為ViewPager的緩存機制,會同時創建三個Fragment的實例,所以打印了三條isVisibleToUeser: false的日志,因為選中的是MeiziDataFragment,所以它還會觸發一次setUserVisibleHint(),并且打印出true。

那么,是否可以在setUserVisibleHint(boolean isVisibleToUser)里進行數據加載操作來實現懶加載呢?

可以是可以,如果你只是需要數據的懶加載的話,但如果你還有以下的需求,那么這種方式就不行了:

1、如果你在Fragment可見時需要進行一些控件的操作,比如顯示加載控件
2、如果你還需要在Fragment從 “可見 -> 不可見” 時進行一些操作的話,比如取消加載控件顯示

這邊再提一下,setUserVisibleHint()可能會在Fragment的生命周期之外被調用,也就是可能在view創建前就被調用,也可能在destroyView后被調用,所以如果涉及到一些控件的操作的話,可能會報 null 異常,因為控件還沒初始化,或者已經摧毀了。

進一步封裝

題主稍微進行了一些封裝,自定義了一個新的回調函數 onFragmentVisibleChange(boolean isVisible) ,可以實現的效果有:

1、只有兩種情況會觸發該函數
2、一種是Fragment從“不可見 -> 可見” 時觸發,并傳入 isVisible = true
3、一種是Fragment從“可見 -> 不可見” 時觸發,并傳入 isVisible = false
4、可以在該函數內進行控件的操作,不會報null異常
日志圖3

題主這次仍舊是從DayDataFragment 跳轉到 MeiziDataFragment, 但跟上上面的日志圖片不同,這里只打印了兩條日志,也就是說即使有三個Fragment被實例化了,但只有顯示的那個Fragment和離開的那個Fragment才會觸發回調函數,這樣就可以支持我們在可見狀態變化時進行一些操作,因為不會有多余的false觸發。

另外,因為ViewPager緩存機制,所以題主進行了view的復用,防止onCreateView()重復的創建view,其實也就是將view設置為成員變量,創建view時先判斷是否為null。因為ViewPager里對Fragment的回收和創建時,如果Fragment已經創建過了,那么只會調用 onCreateView() -> onDestroyView() 生命函數,onCreate()和onDestroy并不會觸發,所以關于變量的初始化和賦值操作可以在onCreate()里進行,這樣就可以避免重復的操作。

代碼


2016-04-21 更新:該博客封裝的懶加載實現有些不足,比如不支持數據只有第一次打開Fragment時才進行加載的應用場景,因此重新寫了篇博客,可以移步至此觀看:再來一篇Fragment的懶加載(只加載一次哦)


最后附上代碼,另外注意一下,題主是從項目里抽出代碼,進行一些修改,讓它盡量可以直接復制粘貼使用,但并沒有進行過測試,所以如果不行的話可以留言,題主會查看。或者你直接到我原項目里去查看,代碼已托管至Github上,因為項目是針對具體需求的,所以類里面會增加很多其他無關的代碼。再或者,你可以嘗試自己進行封裝下,代碼很少,不到50行,理解思路就行了。


/**
 * Created by dasu on 2016/9/27.
 * https://github.com/woshidasusu/Meizi
 *
 * Viewpager + Fragment情況下,fragment的生命周期因Viewpager的緩存機制而失去了具體意義
 * 該抽象類自定義一個新的回調方法,當fragment可見狀態改變時會觸發的回調方法,介紹看下面
 *
 * @see #onFragmentVisibleChange(boolean)
 */
public abstract class ViewPagerFragment extends Fragment {

    /**
     * rootView是否初始化標志,防止回調函數在rootView為空的時候觸發
     */
    private boolean hasCreateView;
    
    /**
     * 當前Fragment是否處于可見狀態標志,防止因ViewPager的緩存機制而導致回調函數的觸發
     */
    private boolean isFragmentVisible;
    
    /**
     * onCreateView()里返回的view,修飾為protected,所以子類繼承該類時,在onCreateView里必須對該變量進行初始化
     */
    protected View rootView;

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.d(getTAG(), "setUserVisibleHint() -> isVisibleToUser: " + isVisibleToUser);
        if (rootView == null) {
            return;
        }
        hasCreateView = true;
        if (isVisibleToUser) {
            onFragmentVisibleChange(true);
            isFragmentVisible = true;
            return;
        }
        if (isFragmentVisible) {
            onFragmentVisibleChange(false);
            isFragmentVisible = false;
        }
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initVariable();
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (!hasCreateView && getUserVisibleHint()) {
            onFragmentVisibleChange(true);
            isFragmentVisible = true;
        }
    }

    private void initVariable() {
        hasCreateView = false;
        isFragmentVisible = false;
    }

    /**************************************************************
     *  自定義的回調方法,子類可根據需求重寫
     *************************************************************/

    /**
     * 當前fragment可見狀態發生變化時會回調該方法
     * 如果當前fragment是第一次加載,等待onCreateView后才會回調該方法,其它情況回調時機跟 {@link #setUserVisibleHint(boolean)}一致
     * 在該回調方法中你可以做一些加載數據操作,甚至是控件的操作,因為配合fragment的view復用機制,你不用擔心在對控件操作中會報 null 異常
     *
     * @param isVisible true  不可見 -> 可見
     *                  false 可見  -> 不可見
     */
    protected void onFragmentVisibleChange(boolean isVisible) {
        Log.w(getTAG(), "onFragmentVisibleChange -> isVisible: " + isVisible);
    }
}


用法

新建類ViewPagerFragment,將上面代碼復制粘貼進去,添加需要的import語句 -> 新建你需要的Fragment類,繼承ViewPagerFragment,在onCreateView()里對rootView進行初始化 -> 重寫onFragmentVisibleChange(),在這里進行你需要的操作,比如數據加載,控制顯示等。

public class MyFragment extends ViewPagerFragment{
    
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (rootView == null) {
            rootView = inflater.inflate(R.layout.fragment_android, container, false);
        
        }
        return rootView;
    } 
    
    @Override
    protected void onFragmentVisibleChange(boolean isVisible) {
        super.onFragmentVisibleChange(isVisible);
        if (isVisible) {
        //   do things when fragment is visible    
        //    if (ListUtils.isEmpty(mDataList) && !isRefreshing()) {
        //        setRefresh(true);
        //        loadServiceData(false);
            } else {
        //        setRefresh(false);
            }
        }
    }
}
  

項目Github 地址


PS

以上就是這次的內容了,最近題主想利用 Gank公開的api,做一個類似于Meizi的應用出來,雖然這個App已經有無數人都做過了,但確實是一個很值得學習的項目,題主仍然是小白一個,所以還是好好學習下。drakeet的Meizi項目用到了很多高級技術,比如Rxjava之類的,題主看不懂,其他Github上一些比較出名的Meizi App要么是MVP架構,要么還是用到了目前小白的我看不懂的技術,所以這次就決定自己用最基礎的MVC,一些簡單常用的第三方庫來做這個App,畢竟路要一步一步走,如果這個完成了,收獲和體驗應該會很多(這不就收獲了這篇隨筆了嗎O(∩_∩)O),所以,如果有興趣的話,歡迎Start,歡迎指點,歡迎拍磚,大家一起學習進步。


QQ圖片20180316094923.jpg

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容