Paging Library-初探分頁庫

簡介

在2018年5月9日的谷歌開發者大會(Google I/O 2018) 中提出在去年發佈的廣受歡迎的架構組件上,進一步改進並推出了Jetpack。Jetpack能幫助我們更專注提升應用體驗,加快應用開發速度,處理類似後臺任務、UI 導航以及生命週期管理等。

發佈的新版 Android Jetpack 組件中更新的內容包括 4 個部分:WorkManagerPagingNavigation 以及 Slices

我們今天要說的就是Paging,在進行大數據查詢的時候,Paging分頁組件可以讓我們從本地或者網絡中通過漸進的方式、逐步的請求數據加載,在不過多增加設備負擔或等待時間的情況下,讓應用擁有了處理大型數據的能力,其中包括對RecycleView的支持。

會從以下幾個方面去介紹:

  1. paging的整體流程
  2. paging主要類的功能
  3. 如何使用paging

在開始之前我們需要搞明白一個問題

爲什麼使用paging,或者paging的優點

在一個新技術出來的時候,我們不能盲目的去使用,不能爲了使用而使用,引入新技術是有一定的成本的。所以在真正的工作項目中引入一個新技術,前期一定要調研好,要明確引入後,利大於弊,才能着手去做。
迴歸正題,我大概總結以下幾點:

  1. 業務和UI高度分離,更加靈活
  2. 引入LiveData,具有了生命週期
  3. 線程切換,數據處理在子線程,顯示在UI線程
  4. 封裝了實現,對於應用來說更加方便

paging整體流程

在這裏插入圖片描述
上圖是非常棒的官方原理圖,通過原理圖,能很清晰的看到數據的傳輸過程,已經設計到的線程變動。

  1. DataSource 在IO線程中獲取數據,數據可以是本地數據庫或者網絡請求返回
  2. DataSource將數據通過主線程線程傳遞給PagedList
  3. pagedList然後將數據傳遞給DiffUtil
  4. DiffUtil在對比現在的item和新建item的差異,這個過程是在異步線程中進行的
  5. 對比結束,通過PagedListAdapter調用notifyItemInserted()將新的數據插入到適當的位置
  6. RecycleView收到通知後會更新視圖。

paging幾個主要類的功能

DataSource

數據源,數據的改變會驅動列表的更新。

Datasource提供了三種實現的類:

  • PageKeyedDataSource:下一次的請求依賴於上一次的某個值,例如:在平時的分頁接口中,loadmore時,需要上一次接口返回的cursor值作爲這次的參數進行請求。
  • ItemKeyedDataSource:下一條的數據依賴於上一條的數據。
  • PositionalDataSource:請求從具體位置開始,例如:我要請求從第20個數據開始的列表。

根據使用場景選擇實現DataSource不同的抽象類,使用時需要實現請求加載數據的方法。其中這三種都需要實現loadInitial()方法,個字都封裝了請求初始化數據的參數類型LoadInitialParams。不同是分享加載數據的方法,PageKeyedDataSourceItemKeyedDataSrouce比較相似,需要實現loadBefore()和loadAfter()方法,同樣對請求參數做了封裝,即LoadParams。PositionalDataSource需要實現loadRange()

PagedList

核心類,它從數據源取出數據,同時,它負責控制 第一次默認加載多少數據,之後每一次加載多少數據,如何加載等等,並將數據的變更反映到UI上。

Config:配置PagedList從Datasource加載數據的方式,其中包含以下屬性

PagedList.Config build = new PagedList.Config.Builder()
                .setEnablePlaceholders(true) // 數據爲null時是否使用佔位符
                .setPrefetchDistance(pageSize) // 當距離底部多少個數據時,開始加載數據
                .setInitialLoadSizeHint(pageSize * 2) // 第一次加載時的數量
                .setPageSize(pageSize) // 每一次加載的數量
                .build();

PagedListAdapter

適配器,RecyclerView的適配器,通過DiffUtil來分析數據的變化,然後通過相應的方法刷新UI(增加/刪除/替換等)

PagedListAdapter是很簡單,僅僅是一個被代理的對象,所有的相關邏輯都委託了給AsyncPagedListDiffer

public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {
    private final AsyncPagedListDiffer<T> mDiffer;
    private final AsyncPagedListDiffer.PagedListListener<T> mListener =
            new AsyncPagedListDiffer.PagedListListener<T>() {
        @Override
        public void onCurrentListChanged(@Nullable PagedList<T> currentList) {
            PagedListAdapter.this.onCurrentListChanged(currentList);
        }
    };

    protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
        mDiffer.mListener = mListener;
    }

    @SuppressWarnings("unused, WeakerAccess")
    protected PagedListAdapter(@NonNull AsyncDifferConfig<T> config) {
        mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
        mDiffer.mListener = mListener;
    }

    public void submitList(PagedList<T> pagedList) {
        mDiffer.submitList(pagedList);
    }

    @Nullable
    protected T getItem(int position) {
        return mDiffer.getItem(position);
    }

    @Override
    public int getItemCount() {
        return mDiffer.getItemCount();
    }

    @Nullable
    public PagedList<T> getCurrentList() {
        return mDiffer.getCurrentList();
    }

    @SuppressWarnings("WeakerAccess")
    public void onCurrentListChanged(@Nullable PagedList<T> currentList) {
    }
}

當數據源發生改變時,通過submitList方法,將數據傳遞給,AsyncPagedListDiffer類進行處理,最後進行刷新。這裏使用到了DiffUtil.ItemCallback進行數據比對,再也不是無腦的全部刷新一遍了,想詳細的介紹看這篇博客

加載數據的來源

數據的加載方式主要有兩種:

單一數據來源:本地數據或者是網絡數據

通過LivePagedListBuilder來創建LiveData爲UI提供數據。如果數據源的DB,當數據發生變化的時候,數據庫會推送一個新的PagedList(依賴LiveData機制),網絡請求機制相同,也是會通過LiveData發送PagedList。

多個數據源:本地數據+網絡數據

一般是先加載本地數據,然後加載網絡數據。比如 IM 中的聊天消息,當打開聊天界面時先加載本地數據庫中的聊天消息,加載完了再加載網絡的離線消息。這個時候我們需要爲 PagedList 設置 BoundaryCallback來監聽本地數據是否加載完成,當本地數據加載完成就觸發加載網絡數據,然後入庫,此時LiveData會推送一個新的PagedList, 並觸發界面刷新。
在這裏插入圖片描述
通過LivePagedListBuilder來創建DataSource,DataSource首先從DataBase中獲取數據,通過BoundaryCallback來獲取是否加載完成DB的數據,然後請求網絡數據,請求完成後會將之前的所有的數據通過LiveData發送給PagedListAdapter,通過對數據進行比對,最後刷新到ui上。

如何使用到項目中

上面兩個章節簡單介紹了Paging的整個流程以及一個重要類的作用,接下來讓我們通過Google的官方demo,來學習使用的paging的姿勢。

1、引入paging的庫的依賴,現在paging庫已經更新到1.0.1。

implementation "android.arch.paging:runtime:1.0.1"

2、創建DataSource的實現類,這裏我封裝的是PageKeyedDataSource,這裏和業務有關,因爲現在的業務中,大部分的列表請求都是需要依據之前的cursor值,這個場景正好符合PageKeyedDataSource的定義。

public abstract class BasePageKeyedDataSource<Key, Value, T extends PagingData<Key, Value>> extends PageKeyedDataSource<Key, Value> {

    // 網絡狀態的狀態機
    public MutableLiveData<NetworkState> mNetworkState = new MutableLiveData<>();
    // 是否已經初始化的狀態機
    public MutableLiveData<NetworkState> mInitialLoad = new MutableLiveData<>();
    
    // 首次請求返回的結果
    public abstract T getInitResponse() throws Exception;

    // loadmore返回的結果
    public abstract T getAfterResponse(Key key) throws Exception;

    // 用於重試,在一次請求失敗的時候,可以用於重試接口
    Runnable retry;

    public void retryAllFailed() {
        Runnable preRetry = retry;
        retry = null;
        if (preRetry != null) {
            preRetry.run();
        }
    }


    @Override
    public void loadInitial(@NonNull final LoadInitialParams<Key> params, @NonNull final LoadInitialCallback<Key, Value> callback) {
        mNetworkState.postValue(NetworkState.Companion.getLOADING());
        mInitialLoad.postValue(NetworkState.Companion.getLOADING());
        try {
            T response = getInitResponse();
            mNetworkState.postValue(NetworkState.Companion.getLOADED());
            mInitialLoad.postValue(NetworkState.Companion.getLOADED());
            callback.onResult(response.data(), null, response.after());
        } catch (Exception e) {
            if (e instanceof NoMoreDataException) {
                mNetworkState.postValue(NetworkState.Companion.getNO_DATA());
                return;
            }
            retry = new Runnable() {
                @Override
                public void run() {
                    loadInitial(params, callback);
                }
            };
            NetworkState error = NetworkState.Companion.error(TextUtils.isEmpty(e.getMessage()) ? "unknown error " : e.getMessage());
            mNetworkState.postValue(error);
            mInitialLoad.postValue(error);
        }
    }

    @Override
    public void loadBefore(@NonNull LoadParams<Key> params, @NonNull LoadCallback<Key, Value> callback) {
        // ignored, since we only ever append to our initial load
    }

    @Override
    public void loadAfter(@NonNull final LoadParams<Key> params, @NonNull final LoadCallback<Key, Value> callback) {
        mNetworkState.postValue(NetworkState.Companion.getLOADING());
        try {
            T response = getAfterResponse(params.key);
            mNetworkState.postValue(NetworkState.Companion.getLOADED());
            callback.onResult(response.data(), response.after());
        } catch (Exception e) {
            if (e instanceof NoMoreDataException) {
                mNetworkState.postValue(NetworkState.Companion.getNO_DATA());
                return;
            }
            retry = new Runnable() {
                @Override
                public void run() {
                    loadAfter(params, callback);
                }
            };
            NetworkState error = NetworkState.Companion.error(TextUtils.isEmpty(e.getMessage()) ? "unknown error " : e.getMessage());
            mNetworkState.postValue(error);
        }
    }
}

3、創建DataSourceFactory,DataSource的實現類需要通過Factory的方式去創建。

public abstract class BaseDataSourceFactory<Key, Value> extends DataSource.Factory<Key, Value> {

    MutableLiveData<BasePageKeyedDataSource> sourceLiveData = new MutableLiveData<>();

    @Override
    public DataSource<Key, Value> create() {
        BasePageKeyedDataSource source = getSource();
        sourceLiveData.postValue(source);
        return source;
    }

    protected abstract BasePageKeyedDataSource getSource();

    public abstract void setParams(int pageSize, Object... params);

}

4、創建Repository,實現返回一個直接從網絡加載數據的Listing,並使用該名稱作爲加載上一頁/下一頁數據的關鍵

public class PageModelRepository {

    public static <Key, Value> Listing<Value> createModel(int pageSize, final BaseDataSourceFactory<Key, Value> factory
            , @Nullable PagedList.BoundaryCallback<Value> boundaryCallback) {
        PagedList.Config build = new PagedList.Config.Builder()
                .setEnablePlaceholders(true) // 是否爲null使用佔位符
                .setPrefetchDistance(pageSize)// 距離底部多少數據,需要加載更多數據
                .setInitialLoadSizeHint(pageSize * 2) // 第一次加載數據的數量
                .setPageSize(pageSize)// 每次加載數據的個數
                .build();
        LiveData<PagedList<Value>> livePagedList = new LivePagedListBuilder<Key, Value>(factory, build)
                .setBoundaryCallback(boundaryCallback)
                .build();// 返回一個Livedata作爲載體的PagedList

        Listing<Value> listing = new Listing<>();
        listing.refreshState = Transformations.switchMap(factory.sourceLiveData, new Function<BasePageKeyedDataSource, LiveData<NetworkState>>() {
            @Override
            public LiveData<NetworkState> apply(BasePageKeyedDataSource input) {
                return input.mInitialLoad;
            }
        });
        listing.pagedList = livePagedList;
        listing.networkState = Transformations.switchMap(factory.sourceLiveData, new Function<BasePageKeyedDataSource, LiveData<NetworkState>>() {
            @Override
            public LiveData<NetworkState> apply(BasePageKeyedDataSource input) {
                return input.mNetworkState;
            }
        });

        listing.retry = new Runnable() {
            @Override
            public void run() {
                if (factory.sourceLiveData.getValue() != null) {
                    factory.sourceLiveData.getValue().retryAllFailed();
                }
            }
        };
        listing.refresh = new Runnable() {
            @Override
            public void run() {
                if (factory.sourceLiveData.getValue() != null) {
                    factory.sourceLiveData.getValue().invalidate();
                }
            }
        };
        return listing;
    }
}

5、創建BaseModel,這個類主要封裝了經常使用的網絡狀態,是否正在網絡請求等,目的就是爲了更加簡單的使用paging

public abstract class BasePageModel<Key, Value> extends ViewModel {
    private MutableLiveData<Listing<Value>> result;
    private LiveData<PagedList<Value>> posts;
    private LiveData<NetworkState> networkState;

    private LiveData<NetworkState> refreshState;

    private BaseDataSourceFactory<Key, Value> factory;

    public abstract BaseDataSourceFactory<Key, Value> getFactory();

    public abstract int getPageSize();

    public BasePageModel() {
        result = new MutableLiveData<>();
        factory = getFactory();
        posts = Transformations.switchMap(result, new Function<Listing<Value>, LiveData<PagedList<Value>>>() {
                    @Override
                    public LiveData<PagedList<Value>> apply(Listing<Value> input) {
                        return input.pagedList;
                    }
                }
        );
        networkState = Transformations.switchMap(result, new Function<Listing<Value>, LiveData<NetworkState>>() {
                    @Override
                    public LiveData<NetworkState> apply(Listing<Value> input) {
                        return input.networkState;
                    }
                }
        );
        refreshState = Transformations.switchMap(result, new Function<Listing<Value>, LiveData<NetworkState>>() {
                    @Override
                    public LiveData<NetworkState> apply(Listing<Value> input) {
                        return input.refreshState;
                    }
                }
        );
    }

    public LiveData<PagedList<Value>> getPosts() {
        return posts;
    }

    public LiveData<NetworkState> getNetworkState() {
        return networkState;
    }

    public LiveData<NetworkState> getRefreshState() {
        return refreshState;
    }
    
    // 初始化數據,並拉取數據
    public void startFetchData(Object... objects) {
        factory.setParams(getPageSize(), objects);
        if (result.getValue() == null) {
            result.setValue(PageModelFactory.createModel(getPageSize(), factory, getBoundaryCallback()));
        }
    }

    // 只有在雙數據源的時候才需要實現這個類
    public abstract PagedList.BoundaryCallback<Value> getBoundaryCallback();

    // 刷新數據
    public void refresh() {
        if (result.getValue() != null) {
            result.getValue().refresh.run();
        }
    }

    // 用來重試請求接口
    public void retry() {
        if (result.getValue() != null) {
            result.getValue().retry.run();
        }
    }
}

使用的時候只需要繼承這個類,實現三個函數(getPageSize()、getFactory()、getBondaryCallback()),然後使用的時候直接調用startFetchData,就可以,是不是很簡單。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章