简介
在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
extends RecyclerView.Adapter {
private final AsyncPagedListDiffer mDiffer;
private final AsyncPagedListDiffer.PagedListListener mListener =
new AsyncPagedListDiffer.PagedListListener() {
@Override
public void onCurrentListChanged(@Nullable PagedList currentList) {
PagedListAdapter.this.onCurrentListChanged(currentList);
}
};
protected PagedListAdapter(@NonNull DiffUtil.ItemCallback diffCallback) {
mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
mDiffer.mListener = mListener;
}
@SuppressWarnings("unused, WeakerAccess")
protected PagedListAdapter(@NonNull AsyncDifferConfig config) {
mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
mDiffer.mListener = mListener;
}
public void submitList(PagedList pagedList) {
mDiffer.submitList(pagedList);
}
@Nullable
protected T getItem(int position) {
return mDiffer.getItem(position);
}
@Override
public int getItemCount() {
return mDiffer.getItemCount();
}
@Nullable
public PagedList getCurrentList() {
return mDiffer.getCurrentList();
}
@SuppressWarnings("WeakerAccess")
public void onCurrentListChanged(@Nullable PagedList 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> extends PageKeyedDataSource {
// 网络状态的状态机
public MutableLiveData mNetworkState = new MutableLiveData<>();
// 是否已经初始化的状态机
public MutableLiveData 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 params, @NonNull final LoadInitialCallback 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 params, @NonNull LoadCallback callback) {
// ignored, since we only ever append to our initial load
}
@Override
public void loadAfter(@NonNull final LoadParams params, @NonNull final LoadCallback 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 extends DataSource.Factory {
MutableLiveData sourceLiveData = new MutableLiveData<>();
@Override
public DataSource 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 Listing createModel(int pageSize, final BaseDataSourceFactory factory
, @Nullable PagedList.BoundaryCallback boundaryCallback) {
PagedList.Config build = new PagedList.Config.Builder()
.setEnablePlaceholders(true) // 是否为null使用占位符
.setPrefetchDistance(pageSize)// 距离底部多少数据,需要加载更多数据
.setInitialLoadSizeHint(pageSize * 2) // 第一次加载数据的数量
.setPageSize(pageSize)// 每次加载数据的个数
.build();
LiveData> livePagedList = new LivePagedListBuilder(factory, build)
.setBoundaryCallback(boundaryCallback)
.build();// 返回一个Livedata作为载体的PagedList
Listing listing = new Listing<>();
listing.refreshState = Transformations.switchMap(factory.sourceLiveData, new Function>() {
@Override
public LiveData apply(BasePageKeyedDataSource input) {
return input.mInitialLoad;
}
});
listing.pagedList = livePagedList;
listing.networkState = Transformations.switchMap(factory.sourceLiveData, new Function>() {
@Override
public LiveData 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 extends ViewModel {
private MutableLiveData> result;
private LiveData> posts;
private LiveData networkState;
private LiveData refreshState;
private BaseDataSourceFactory factory;
public abstract BaseDataSourceFactory getFactory();
public abstract int getPageSize();
public BasePageModel() {
result = new MutableLiveData<>();
factory = getFactory();
posts = Transformations.switchMap(result, new Function, LiveData>>() {
@Override
public LiveData> apply(Listing input) {
return input.pagedList;
}
}
);
networkState = Transformations.switchMap(result, new Function, LiveData>() {
@Override
public LiveData apply(Listing input) {
return input.networkState;
}
}
);
refreshState = Transformations.switchMap(result, new Function, LiveData>() {
@Override
public LiveData apply(Listing input) {
return input.refreshState;
}
}
);
}
public LiveData> getPosts() {
return posts;
}
public LiveData getNetworkState() {
return networkState;
}
public LiveData 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 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,就可以,是不是很简单。