簡介
在2018年5月9日的谷歌開發者大會(Google I/O 2018) 中提出在去年發佈的廣受歡迎的架構組件上,進一步改進並推出了Jetpack。Jetpack能幫助我們更專注提升應用體驗,加快應用開發速度,處理類似後臺任務、UI 導航以及生命週期管理等。
發佈的新版 Android Jetpack 組件中更新的內容包括 4 個部分:WorkManager
、Paging
、Navigation
以及 Slices
。
我們今天要說的就是Paging,在進行大數據查詢的時候,Paging分頁組件可以讓我們從本地或者網絡中通過漸進的方式、逐步的請求數據加載,在不過多增加設備負擔或等待時間的情況下,讓應用擁有了處理大型數據的能力,其中包括對RecycleView的支持。
會從以下幾個方面去介紹:
- paging的整體流程
- paging主要類的功能
- 如何使用paging
爲什麼使用paging,或者paging的優點
在一個新技術出來的時候,我們不能盲目的去使用,不能爲了使用而使用,引入新技術是有一定的成本的。所以在真正的工作項目中引入一個新技術,前期一定要調研好,要明確引入後,利大於弊,才能着手去做。
迴歸正題,我大概總結以下幾點:
- 業務和UI高度分離,更加靈活
- 引入LiveData,具有了生命週期
- 線程切換,數據處理在子線程,顯示在UI線程
- 封裝了實現,對於應用來說更加方便
paging整體流程
上圖是非常棒的官方原理圖,通過原理圖,能很清晰的看到數據的傳輸過程,已經設計到的線程變動。
- DataSource 在IO線程中獲取數據,數據可以是本地數據庫或者網絡請求返回
- DataSource將數據通過主線程線程傳遞給PagedList
- pagedList然後將數據傳遞給DiffUtil
- DiffUtil在對比現在的item和新建item的差異,這個過程是在異步線程中進行的
- 對比結束,通過PagedListAdapter調用notifyItemInserted()將新的數據插入到適當的位置
- RecycleView收到通知後會更新視圖。
paging幾個主要類的功能
DataSource
數據源,數據的改變會驅動列表的更新。
Datasource提供了三種實現的類:
PageKeyedDataSource
:下一次的請求依賴於上一次的某個值,例如:在平時的分頁接口中,loadmore時,需要上一次接口返回的cursor值作爲這次的參數進行請求。ItemKeyedDataSource
:下一條的數據依賴於上一條的數據。PositionalDataSource
:請求從具體位置開始,例如:我要請求從第20個數據開始的列表。
根據使用場景選擇實現DataSource不同的抽象類,使用時需要實現請求加載數據的方法。其中這三種都需要實現loadInitial()
方法,個字都封裝了請求初始化數據的參數類型LoadInitialParams
。不同是分享加載數據的方法,PageKeyedDataSource
和ItemKeyedDataSrouce
比較相似,需要實現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,就可以,是不是很簡單。