写在前面
本文分三部分进行解读:首先是官方文档的摘录,介绍Paging库的作用以及使用方式,第二部分会给出一个实例来介绍Paging库的用法,第三部分结合项目对源码进行阅读分析。
The Paging Library helps you load and display small chunks of data at a time. Loading partial data on demand reduces usage of network bandwidth and system resources.
Paging分页库可以帮助你一次加载小块数据,并将它们展示到UI上。根据需求加载部分数据可以减少网络带宽和系统资源的使用。
Library architecture
本节描述、展示paging库的主要组件。
PagedList
PagedList类是paging分页库的关键组件,它负责加载小块的、或是分页的数据。需要更多的数据时,它负责将新加载的数据加入到已有的PagedList对象中。如果任意已加载的数据发生改变时,会将新的PagedList实例,通过LiveData或Rxjava2传递给observable数据holder。在生成PagedList对象时,app UI会继续展示数据,而且会根据lifecycle来更新。
使用 PagedList 对象的 LiveData 存储器加载和显示数据:
public class ConcertViewModel extends ViewModel {
private ConcertDao concertDao;
public final LiveData<PagedList<Concert>> concertList;
// Creates a PagedList object with 50 items per page.
public ConcertViewModel(ConcertDao concertDao) {
this.concertDao = concertDao;
concertList = new LivePagedListBuilder<>(
concertDao.concertsByDate(), 50).build();
}
}
构造可观察列表
通常,您的界面代码会观察 LiveData 对象(如果您使用 RxJava2,则会观察 Flowable 或 Observable 对象),该对象位于您应用的ViewModel中。此可观察对象搭起应用列表数据的呈现与内容之间的关联。
为了创建其中一个可观察的 PagedList 对象,请将 DataSource.Factory 的实例传递到 LivePagedListBuilder 或 RxPagedListBuilder对象。DataSource 对象会加载单个PagedList的页面。Factory类会创建新的 PagedList 实例来响应内容更新,例如数据库表失效和网络刷新。Room 持久性库可为您提供 DataSource.Factory 对象,您也可以构建自己的对象。
以下代码段展示了如何使用 Room 的 DataSource.Factory 构建功能在应用的 ViewModel 类中创建新的 LiveData 实例:
// ConcertDao
@Dao
public interface ConcertDao {
// The Integer type parameter tells Room to use a PositionalDataSource
// object, with position-based loading under the hood.
@Query("SELECT * FROM concerts ORDER BY date DESC")
DataSource.Factory<Integer, Concert> concertsByDate();
}
// ConcertViewModel
// The Integer type argument corresponds to a PositionalDataSource object.
DataSource.Factory<Integer, Concert> myConcertDataSource =
concertDao.concertsByDate();
LiveData<PagedList<Concert>> concertList =
LivePagedListBuilder(myConcertDataSource, /* page size */ 50).build();
自定义分页配置
要进一步为高级用例配置 LiveData,您还可以定义自己的分页配置。特别是,您可以定义以下特性:
页面大小:每个页面中的项数。
预取距离:给定应用界面中的最后一个可见项,分页库应尝试提前获取的超出此最后一项的项数。此值应是页面大小的数倍大。
占位符存在:确定界面是否对尚未完成加载的列表项显示占位符。有关使用占位符的优缺点的探讨,请参阅如何在界面中提供占位符。
如果您希望更好地控制分页库何时从应用数据库加载列表,请将自定义 Executor 对象传递给 LivePagedListBuilder,如以下代码段所示:
// ConcertViewModel
PagedList.Config myPagingConfig = new PagedList.Config.Builder()
.setPageSize(50)
.setPrefetchDistance(150)
.setEnablePlaceholders(true)
.build();
// The Integer type argument corresponds to a PositionalDataSource object.
DataSource.Factory<Integer, Concert> myConcertDataSource =
concertDao.concertsByDate();
LiveData<PagedList<Concert>> concertList =
new LivePagedListBuilder<>(myConcertDataSource, myPagingConfig)
.setFetchExecutor(myExecutor)
.build();
选择正确的数据源类型
请务必连接到能最好地处理源数据结构的数据源:
1.如果您加载的网页嵌入了上一页/下一页的键,请使用 PageKeyedDataSource。例如,如果您从网络中获取社交媒体帖子,则可能需要将一个 nextPage 令牌从一次加载传递到后续加载。
2.如果您需要使用项目 N 中的数据来获取项目 N+1,请使用 ItemKeyedDataSource。例如,如果您要为讨论应用获取会话式评论,则可能需要传递最后一条评论的 ID 以获取下一条评论的内容。
3.如果您需要从数据存储区中选择的任意位置获取数据页,请使用 PositionalDataSource。该类支持从您选择的任意位置开始请求一组数据项。例如,该请求可能会返回从位置 1500 开始的 50 个数据项。
数据无效时发送通知
当使用分页库时,由数据层在表或行已过时通知应用的其他层。为此,请从您为应用选择的 DataSource 类中调用 invalidate()。
项目实战
这里我使用了 玩Android开放API 来编写项目demo,展示的数据也是WanAndroid首页列表数据,如下图所示:
附上项目地址:FFmpegInAndroid
这里数据是从网络获取,以下是一些代码准备:
1.封装的网络库
网络库使用Retrofit,封装了玩Android API,最后将接口调用封装在了RequestUtil之中
public class RetrofitFactory {
······
// base
private static final String BASE_URL_WANANDROID = "https://www.wanandroid.com/";
······
private Retrofit createRetrofit(String baseUrl) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(new PostParamsConvertInterceptor());// post请求时,将FormBody转为json格式
if (Utils.isAppDebug()) {
builder.addInterceptor(new LogInterceptor()); // 打印日志 拦截器: 在中间添加LogInterceptor,打印出来的url有公共参数,且返回数据已解密
}
builder.retryOnConnectionFailure(true)
.connectTimeout(DEFAULT_CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(DEFAULT_READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(DEFAULT_WRITE_TIMEOUT, TimeUnit.SECONDS);
return new Retrofit.Builder()
.client(builder.build())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(baseUrl)
.build();
}
······
public static WanApiService getWanApiService(){
return getInstance().mWanApiService;
}
}
// 存放API
public interface WanApiService {
······
/**
* 首页文章列表---同步
* @param page 页码,拼接在连接中,从0开始。
*/
@GET(value = "article/list/{page}/json")
Call<BaseResponse<WanHomeData>> getWanAndroidHomePageDataSync(@Path("page") int page);
}
// 调用API
public class RequestUtil {
······
/**
* 首页文章列表---同步
* @param page 页码,拼接在连接中,从0开始。
*/
public static BaseResponse<WanHomeData> getWanAndroidHomePageDataSync(int page) {
BaseResponse<WanHomeData> baseResponse;
try {
baseResponse = RxHelper.parseResponse(RetrofitFactory.getWanApiService()
.getWanAndroidHomePageDataSync(page).execute());
} catch (IOException e) {
LogUtils.e(e);
baseResponse = BaseResponse.build(e.getMessage());
}
return baseResponse;
}
}
2.Paging
看代码之前先看一下这部分的结构:Activity的ViewModel中的Repository负责数据的管理,其中的LiveData<PagedList即为Paging库的使用,且初始化时在Activity中对其进行了observe。从网络上获取了数据之后,数据通知Activity,传至Adapter更新数据。
首先Activity和ViewModel中,我们获取到了PagedList的外层LiveData并观察
// Activity
public class WanHomeActivity extends AppCompatActivity {
······
private void initViewModel() {
mViewModel = ViewModelProviders.of(this).get(WanHomeViewModel.class);
// 观察LiveData<PagedList<DataBean>
mViewModel.getRepository().getPagedFeed().observe(this, new Observer<PagedList<WanHomeData.DatasBean>>() {
@Override
public void onChanged(PagedList<WanHomeData.DatasBean> wanHomeData) {
// adapter接收新数据
adapter.submitList(wanHomeData);
}
});
······
}
}
// ViewModel
public class WanHomeViewModel extends ViewModel {
private WanAndroidRepository mRepository;
public WanHomeViewModel() {
mRepository = new WanAndroidRepository();
}
public WanAndroidRepository getRepository() {
return mRepository;
}
}
在Repository中,我们负责对数据进行获取、处理、通知。因为我们使用的是玩Android的首页API,获取数据需要传递页码,所以选用PageKeyedDataSource作为处理源数据结构的数据源:
// Repository
public class WanAndroidRepository {
private LiveData<PagedList<WanHomeData.DatasBean>> pagedFeed;
private MutableLiveData<WanHomeStatus> mWanHomeStatus;
private int mPage = 0;
public WanAndroidRepository() {
mWanHomeStatus = new MutableLiveData<>();
// PaginedList的配置信息,设置了size、初始加载数目、剩余多少加载下一页
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(50)
.setEnablePlaceholders(true)
.setInitialLoadSizeHint(50)
.setPrefetchDistance(10)
.build();
pagedFeed = new LivePagedListBuilder<>(new WanHomeDataSourceFactory(), config).build();
}
······
// 获取首页数据:RequestUtil.getWanAndroidHomePageDataSync(page)是同步进行获取
private WanHomeData loadFeedFromServer(int page) {
BaseResponse<WanHomeData> feedBaseResponse = RequestUtil.getWanAndroidHomePageDataSync(page);
if (feedBaseResponse.data == null) {
mWanHomeStatus.setValue(new WanHomeStatus(feedBaseResponse));
}
return feedBaseResponse.data;
}
class WanHomeDataSourceFactory extends DataSource.Factory {
@NonNull
@Override
public DataSource create() {
return new WanHomePageDataSource();
}
}
// 因为我们使用的是玩Android的首页API,获取数据需要传递页码,所以选用PageKeyedDataSource作为处理源数据结构的数据源
class WanHomePageDataSource extends PageKeyedDataSource<Integer, WanHomeData.DatasBean> {
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params, final @NonNull LoadInitialCallback<Integer, WanHomeData.DatasBean> callback) {
mPage = 1;
// get data
WanHomeData wanHomeData = loadFeedFromServer(mPage);
if (wanHomeData != null) {
// 通过callback回调将结果通知出去
callback.onResult(wanHomeData.datas, 0, wanHomeData.datas.size(), mPage, mPage + 1);
}
}
@Override
public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, WanHomeData.DatasBean> callback) {
// do nothing
}
@Override
public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, WanHomeData.DatasBean> callback) {
mPage = params.key;
// get data
WanHomeData wanHomeData = loadFeedFromServer(mPage);
if (wanHomeData != null && wanHomeData.datas != null && wanHomeData.datas.size() > 0) {
// 通过callback回调将结果通知出去
callback.onResult(wanHomeData.datas, mPage + 1);
}
}
}
}
Adapter中不同之处在于,继承PagedListAdapter,数据源不再显式的放置,而是通过getItem(int pos)来获取;Adapter的构造函数中需要传入DiffUtil.ItemCallback用于判断两个item是否相同(去重)
// Adapter
public class WanHomeAdapter extends PagedListAdapter<WanHomeData.DatasBean, WanHomeAdapter.WanHomeViewHolder> {
public WanHomeAdapter() {
super(DIFF_Callback);
}
@NonNull
@Override
public WanHomeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new WanHomeViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_wan_android_home_page, parent, false));
}
@Override
public void onBindViewHolder(@NonNull WanHomeViewHolder holder, int position) {
WanHomeData.DatasBean bean = getDataBean(position);
······ // setData
}
private WanHomeData.DatasBean getDataBean(int pos) {
if (getItemCount() == 0 || pos >= getItemCount()) {
return null;
} else {
return getItem(pos);
}
}
class WanHomeViewHolder extends RecyclerView.ViewHolder {
······
public WanHomeViewHolder(@NonNull View itemView) {
super(itemView);
······
}
}
private static final DiffUtil.ItemCallback<WanHomeData.DatasBean> DIFF_Callback = new DiffUtil.ItemCallback<WanHomeData.DatasBean>() {
@Override
public boolean areItemsTheSame(@NonNull WanHomeData.DatasBean oldItem, @NonNull WanHomeData.DatasBean newItem) {
return oldItem.id == newItem.id;
}
@Override
public boolean areContentsTheSame(@NonNull WanHomeData.DatasBean oldItem, @NonNull WanHomeData.DatasBean newItem) {
return oldItem.apkLink.equals(newItem.apkLink);
}
};
}
源码分析
根据上一部分的实例代码,我们大致了解了Paging的使用过程,那么就先从Repository这里作为入口开始分析。首先,Repository中实例化的对象为LiveData<PagedList<WanHomeData.DatasBean>>
,我们来看一下初始化的过程:
1.PagedList实例化
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(50) // 设置每次从DataSource中加载多少条数据
.setEnablePlaceholders(true) // 是否启用placeholder
.setInitialLoadSizeHint(50) // 第一次加载多少条
.setPrefetchDistance(10) // 距离已加载数据末尾多少条时,加载下一组数据
.build();
pagedFeed = new LivePagedListBuilder<>(new WanHomeDataSourceFactory(), config).build();
接下来看一下new LivePagedListBuilder以及build
// LivePagedListBuilder
public LivePagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
@NonNull PagedList.Config config) {
//noinspection ConstantConditions
if (config == null) {
throw new IllegalArgumentException("PagedList.Config must be provided");
}
//noinspection ConstantConditions
if (dataSourceFactory == null) {
throw new IllegalArgumentException("DataSource.Factory must be provided");
}
mDataSourceFactory = dataSourceFactory;
mConfig = config;
}
// build
@NonNull
public LiveData<PagedList<Value>> build() {
return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
}
参数分析
mInitialLoadKey:从DataSource中加载数据时提供的数据类型;如果使用的是位置类的DataSource,这个Key必须是Integer类型
mConfig:前面初始化的Config
mBoundaryCallback:如果你加载的是本地数据,当本地数据耗尽时,如果callback不为null,则会通过callback去加载数据(比如网络加载)
mDataSourceFactory:DataSourceFactory,详见后文
Executor:主线程
mFetchExecutor:IO线程
前面的操作更多的是准备,接下来我们来看create函数:
// No work (such as loading) is done immediately, the creation of the first PagedList is is deferred until the LiveData is observed.
@AnyThread
@NonNull
private static <Key, Value> LiveData<PagedList<Value>> create(
@Nullable final Key initialLoadKey,
@NonNull final PagedList.Config config,
@Nullable final PagedList.BoundaryCallback boundaryCallback,
@NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
@NonNull final Executor notifyExecutor,
@NonNull final Executor fetchExecutor) {
// 一个LiveData类,可以在有活动观察者的情况下invalidated 和computed
// 可以通过{@link#invalidate()}使其失效,如果存在active observer或当他们开始observe时,则将导致对{@link#compute())}的调用
return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
@Nullable
private PagedList<Value> mList;
@Nullable
private DataSource<Key, Value> mDataSource;
private final DataSource.InvalidatedCallback mCallback =
new DataSource.InvalidatedCallback() {
@Override
public void onInvalidated() {
invalidate();
}
};
@SuppressWarnings("unchecked") // for casting getLastKey to Key
@Override
protected PagedList<Value> compute() {
// 以PageKeyedDataSource为例,加载下一页时传递的key为页码
@Nullable Key initializeKey = initialLoadKey;
if (mList != null) {
initializeKey = (Key) mList.getLastKey();
}
// 获取有效的PagedList,详解见下文
do {
if (mDataSource != null) {
mDataSource.removeInvalidatedCallback(mCallback);
}
// 实例化datasource
mDataSource = dataSourceFactory.create();
mDataSource.addInvalidatedCallback(mCallback);
mList = new PagedList.Builder<>(mDataSource, config)
.setNotifyExecutor(notifyExecutor)
.setFetchExecutor(fetchExecutor)
.setBoundaryCallback(boundaryCallback)
.setInitialKey(initializeKey)
.build();
} while (mList.isDetached());
return mList;
}
}.getLiveData();
}
do-while:
在create函数中,我们实例化了一个ComputableLiveData<PagedList>对象,并实现了该对象的compute函数。compute函数的主体部分使用了do-while循环来包裹,原因在abstract class PagedList的头部注释中我们可以找到:
A PagedList is mutable while loading, or ready to load from its DataSource.As loads succeed, a mutable PagedList will be updated via Runnables on the main thread. You can listen to these updates with a {@link Callback}. (Note that {@link PagedListAdapter} will listen to these to signal RecyclerView about the updates/changes).
PagedList在加载时或者准备从其DataSource加载时是可修改的。加载成功后,可变的PagedList将通过主线程上的Runnables进行更新。 您可以使用{@link Callback}接收这些更新。(请注意,{@ link PagedListAdapter}将监听这些内容,以向RecyclerView发出有关更新/更改的信号)。
If a PagedList attempts to load from an invalid DataSource, it will {@link #detach()} from the DataSource, meaning that it will no longer attempt to load data. It will return true from {@link #isImmutable()}, and a new DataSource / PagedList pair must be created to load
further data. See {@link DataSource} and {@link LivePagedListBuilder} for how new PagedLists are created to represent changed data.
如果PagedList尝试从无效的DataSource加载,它将{@link #detach()}从DataSource加载,这意味着它将不再尝试加载数据。 它将从{@link #isImmutable())}返回true,并且必须创建一个新的DataSource/PagedList对以进行加载
进一步的数据。有关如何创建新的PagedList来表示更改的数据,请参见{@link DataSource}和{@link LivePagedListBuilder}。
而且ComputableLiveData中invalidate()的描述如下:
/**
* Invalidates the LiveData.
* <p>
* When there are active observers, this will trigger a call to {@link #compute()}.
*/
// 使LiveData无效。当有活动的观察者时,这将触发对{@link #compute()}的调用。
public void invalidate() {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}
从这里我们可以得知:compute()函数用于当前LiveData无效化时找到当前的PagedList对象;而PagedList在加载时或者准备从其DataSource加载时是可修改的,也就是当前获取到的PagedList不一定是有效的PagedList,而判断的条件即为mList.isDetached(),故循环获取直至拿到有效的对象。
dataSourceFactory.create()
我们先重新看一下第二部分的项目实战中的FactoryDataSource:
我们先来看一下DataSource的源码中的注释:
1.总览
用于将快照数据页面加载到{@link PagedList}中的基类。
查询数据源以将内容页面加载到{@link PagedList}中。PagedList可以随着加载更多数据而增长,但是加载的数据无法更新。如果基础数据集被修改,则必须创建一个新的PagedList / DataSource对以表示新数据。
2.加载页面
PagedList响应加载提示从其DataSource查询数据。当用户在RecyclerView中滚动时,{@ link PagedListAdapter}调用{@link PagedList#loadAround(int)}以加载内容。
要控制PagedList从其数据源查询数据的方式和时间,请参见{@link PagedList.Config}。 Config对象定义诸如负载大小和预取距离之类的东西。
3.更新分页数据
PagedList / DataSource对是数据集的快照。如果发生更新(例如重新排序,插入,删除或内容更新),则必须创建一对新的PagedList / DataSource。数据源必须检测到无法继续加载其快照(例如,当数据库查询发现表无效时),然后调用{@link #invalidate())}。然后将创建一个新的PagedList / DataSource对,以从数据库查询的新状态加载数据。
如果您有更细致的更新信号,例如网络API发出对列表中单个项目的更新信号,则建议将数据从网络加载到内存中。然后通过包装内存中快照的数据源将数据提供给PagedList。每次内存中副本更改时,都将使先前的数据源无效,并且可以创建一个新的包装快照的新状态。
4.实现数据源
要实现,请扩展以下子类之一:{@link PageKeyedDataSource},{@ link ItemKeyedDataSource}或{@link PositionalDataSource}。
如果您加载的页面嵌入了用于加载相邻页面的键,请使用{@link PageKeyedDataSource}
。例如,返回一些项目的网络响应以及下一个/上一个页面链接。
如果您需要使用项目{@code N-1}中的数据来加载项目{@code N},请使用{@link ItemKeyedDataSource}
。例如,如果向后端请求列表中的下一个注释需要最新加载的注释的ID或时间戳,或者从名称排序的数据库查询中查询下一个用户则需要前一个的名称和唯一ID。
如果您可以在任意位置加载所需大小的页面并提供固定的项目计数,请使用{@link PositionalDataSource}
。PositionalDataSource支持在任意位置查询页面,因此可以按任意顺序向PagedLists提供数据。请注意,需要PositionalDataSource尊重页面大小才能有效平铺。如果要覆盖页面大小(例如,当仅在运行时知道网络页面大小约束时),请使用其他DataSource类之一。
看过了介绍,我们对DataSource有了一个总体的概念,那么接下来我们来看一下DataSource的结构:
首先看一下Factory:
create():
DataSource维护的是数据的一个快照,如果发生更新(例如重新排序,插入,删除或内容更新),则必须创建一对新的PagedList/DataSource,这时就会将DataSource无效化之后,通过该函数来创建一个新的PagedList和数据源,并将新的PagedList通过{@code LiveData }传递给观察者。
map(Function<Value, ToValue> function) & mapByPage(final Function<List, List> function):
将给定函数应用于此Factory产生的DataSources发出的每个值,即map转换输出值。
接下来看一下LoadCallbackHelper:
先说一下他的作用:回调获取到的数据 / error
在上文实例之中,我们在Repository中使用PageKeyedDataSource时,在获取到数据之后,通过他提供的callback将数据传回,就是这里:
那让我们点进去看一下他的实现:
最后调用到的就是我们当前的LoadCallbackHelper。
因为我们项目中用到的是PageKeyedDataSource,所以我们接下来看一下它的具体实现:
initKeys && get/setPreviousKey && get/setNextKey:
这部分有一点,就是两个key注解了@GuardedBy("mKeyLock")
来保证同步。
loadInitial:
首先调用此方法以使用数据初始化PagedList。如果可以统计出可由DataSource加载的item,则建议通过三参数{@link LoadInitialCallback#onResult(List,int,int,Object,Object)}将加载的数据传递给回调。这样PagedLists可以显示来自此源的数据,用显示占位符来表示未加载的item。
loadAfter:
用{@link LoadParams#key LoadParams.key}指定的key获取下一page。返回有效的列表大小要好于返回页面大小,例如后端定义了页面大小。通常,增加加载数量要比减少数量更为安全。数据可以在load方法期间同步传递,也可以在以后的时间延迟并调用。在调用回调之前,将阻止进一步下降的负载。 如果无法加载数据(例如,如果请求无效,或者数据过时且不一致),则调用{@link #invalidate())}使数据源无效并防止进一步加载是有效的。
LoadInitialCallbackImpl:
之前我们看到项目中实现的DataSource中有该类的使用,那么以该类为例我们看一下init的流程
// LoadInitialCallbackImpl
static class LoadInitialCallbackImpl<Key, Value> extends LoadInitialCallback<Key, Value> {
final LoadCallbackHelper<Value> mCallbackHelper;
private final PageKeyedDataSource<Key, Value> mDataSource;
···
@Override
public void onResult(@NonNull List<Value> data, int position, int totalCount,
@Nullable Key previousPageKey, @Nullable Key nextPageKey) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
// setup keys before dispatching data, so guaranteed to be ready
mDataSource.initKeys(previousPageKey, nextPageKey);
int trailingUnloadedCount = totalCount - position - data.size();
if (mCountingEnabled) {
// 跟进
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
data, position, trailingUnloadedCount, 0));
} else {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
}
···
}
// DataSource
void dispatchResultToReceiver(@NonNull PageResult<T> result) {
dispatchToReceiver(result, null, false);
}
private void dispatchToReceiver(final @Nullable PageResult<T> result,
final @Nullable Throwable error, final boolean retryable) {
···
if (executor != null) {
executor.execute(new Runnable() {
@Override
public void run() {
// 跟进
dispatchOnCurrentThread(result, error, retryable);
}
});
} else {
dispatchOnCurrentThread(result, error, retryable);
}
}
@SuppressWarnings("ConstantConditions")
void dispatchOnCurrentThread(@Nullable PageResult<T> result,
@Nullable Throwable error, boolean retryable) {
if (result != null) {
mReceiver.onPageResult(mResultType, result);
} else {
mReceiver.onPageError(mResultType, error, retryable);
}
}
// ContiguousPagedList
class ContiguousPagedList<K, V> extends PagedList<V> implements Callback {
final ContiguousDataSource<K, V> mDataSource;
int mPrependItemsRequested = 0;
int mAppendItemsRequested = 0;
boolean mReplacePagesWithNulls = false;
final boolean mShouldTrim;
Receiver<V> mReceiver = new Receiver<V>() {
@AnyThread
public void onPageResult(int resultType, @NonNull PageResult<V> pageResult) {
if (pageResult.isInvalid()) {
ContiguousPagedList.this.detach();
} else if (!ContiguousPagedList.this.isDetached()) {
List<V> page = pageResult.page;
boolean trimFromFront;
boolean skipNewPage;
if (resultType == 0) {
// 跟进
ContiguousPagedList.this.mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls, pageResult.positionOffset, ContiguousPagedList.this);
if (ContiguousPagedList.this.mLastLoad == -1) {
ContiguousPagedList.this.mLastLoad = pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2;
}
}
···
}
···
}
···
};
}
// PagedStorage
void init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset,
@NonNull Callback callback) {
init(leadingNulls, page, trailingNulls, positionOffset);
callback.onInitialized(size());
}
private void init(int leadingNulls, List<T> page, int trailingNulls, int positionOffset) {
mLeadingNullCount = leadingNulls;
mPages.clear();
mPages.add(page);
mTrailingNullCount = trailingNulls;
mPositionOffset = positionOffset;
mLoadedCount = page.size();
mStorageCount = mLoadedCount;
// initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled
// even if it will break if nulls convert.
mPageSize = page.size();
mNumberPrepended = 0;
mNumberAppended = 0;
}
2.PagedListAdapter
分析完了数据的加载,接下来我们来看一看数据的展示,首先我们的入口在这里:
// PagedListAdapter
public void submitList(@Nullable PagedList<T> pagedList) {
mDiffer.submitList(pagedList);
}
这里调用了AsyncPagedListDiffer的submitList(PagedList):
// AsyncPagedListDiffer
/**
* 将新的PagedList传递给另一个。
*
* 如果已经存在PagedList,则将在后台线程上异步计算差异。
* 计算差异时,将应用差异(将其分配到{@link ListUpdateCallback}),
* 并将新的PagedList交换为{@link #getCurrentList()current list}。
*/
@SuppressWarnings("ReferenceEquality")
public void submitList(@Nullable final PagedList<T> pagedList,
@Nullable final Runnable commitCallback) {
···
final PagedList<T> oldSnapshot = mSnapshot;
final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot();
mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@Override
public void run() {
// 在这里我们计算出了数据的差异
final DiffUtil.DiffResult result;
result = PagedStorageDiffHelper.computeDiff(
oldSnapshot.mStorage,
newSnapshot.mStorage,
mConfig.getDiffCallback());
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
// 跟进
latchPagedList(pagedList, newSnapshot, result,
oldSnapshot.mLastLoad, commitCallback);
}
}
});
}
});
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void latchPagedList(
@NonNull PagedList<T> newList,
@NonNull PagedList<T> diffSnapshot,
@NonNull DiffUtil.DiffResult diffResult,
int lastAccessIndex,
@Nullable Runnable commitCallback) {
if (mSnapshot == null || mPagedList != null) {
throw new IllegalStateException("must be in snapshot state to apply diff");
}
PagedList<T> previousSnapshot = mSnapshot;
mPagedList = newList;
mPagedList.addWeakLoadStateListener(mLoadStateListener);
mSnapshot = null;
// 在更新mPagedList/mSnapshot之后dispatch更新回调
PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
previousSnapshot.mStorage, newList.mStorage, diffResult);
newList.addWeakCallback(diffSnapshot, mPagedListCallback);
if (!mPagedList.isEmpty()) {
// 将最后一个loadAround()索引通过DiffResult传递,将其从旧列表转换为新列表。
// 这样可以确保位置PagedList的lastKey即使在界面中项目没有更改的情况下也可以携带到新列表中(未调用AsyncPagedListDiffer#get)
// 注意:我们不考虑新列表快照和新列表之间的加载,但有一个极罕见的bug:(当占位符被禁用)并且加载开始(出于某种原因)并在差异完成之前完成。
int newPosition = PagedStorageDiffHelper.transformAnchorIndex(
diffResult, previousSnapshot.mStorage, diffSnapshot.mStorage, lastAccessIndex);
// 在此位置触发新列表中的加载,并限制在列表范围内。
// 这是一次加载,而不仅仅是最后加载位置的更新,因为新列表可能不完整。 如果新列表是旧列表的子集,但没填满可视区域,则可能会触发新数据的加载。
mPagedList.loadAround(Math.max(0, Math.min(mPagedList.size() - 1, newPosition)));
}
onCurrentListChanged(previousSnapshot, mPagedList, commitCallback);
}
到这里,新的PagedList已经可以通过{@link #getCurrentList()current list}
被获取到了。最后我们看一下PagedList.loadAround(int)
:
// PagedList
public void loadAround(int index) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size());
}
mLastLoad = index + getPositionOffset();
// 不同的实现
loadAroundInternal(index);
mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
// mLowestIndexAccessed/mHighestIndexAccessed已更新,因此要检查一下是否要调度边界回调。
// 边界回调被推迟到最后一个项目被加载完毕,且用户访问到了边界附近的时候。
// 注意:我们在这里post,因为RecyclerView可能想添加项目作为响应,并且此调用发生在PagedListAdapter绑定中。
tryDispatchBoundaryCallbacks(true);
}
写在最后
笔者能力有限,对Paging整体的理解还不是很好,暂且就分析到这里了~ 感兴趣的小伙伴可以看源码继续研究,里面好多好多很有趣的地方等待着你去发掘~
如果文章中有错误的地方,希望各位大佬们批评指正~
If you like this article, it is written by Johnny Deng.
If not, I don’t know who wrote it.
参考资料
1.官方文档