1.paging库简介
Paging 使您的应用程序配合RecyclerView更容易从数据源中高效优雅地加载所需的数据,不会因为数据库数据量大而造成查询时间过长。说白了就是分页加载的优化。
1.1 目录结构
implementation "androidx.paging:paging-runtime:2.1.2"
之所以没用最新的是因为kotlin版本号冲突,所以降低了版本
1.2 重要的类介绍
paging库最重要的三个类就是DataSource,PageList,PageListAdapter。
(1)PageListAdapter
PagedListAdapter是通过RecyclerView.Adapter实现,用于展示PagedList的数据。它本身并没有比adapter多多少东西。主要需要注意 AsyncPagedListDiffer 这个辅助类。它负责监听PagedList的更新, Item数量的统计等功能。当数据源变动产生新的PagedList,PagedAdapter会在后台线程中比较前后两个PagedList的差异,然后调用notifyItem…()方法更新RecyclerView。具体来看看PagedListAdapter的submitList方法
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);//*******************
}
}
});
}
});
}
void latchPagedList(
@NonNull PagedList<T> newList,
@NonNull PagedList<T> diffSnapshot,
@NonNull DiffUtil.DiffResult diffResult,
int lastAccessIndex,
@Nullable Runnable commitCallback) {
PagedList<T> previousSnapshot = mSnapshot;
mPagedList = newList;
mSnapshot = null;
// dispatch update callback after updating mPagedList/mSnapshot
PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
previousSnapshot.mStorage, newList.mStorage, diffResult);
......
}
最后一行 PagedStorageDiffHelper.dispatchDiff 传进去的第一个参数 mUpdateCallback内部就实现了 mAdapter 的 notifyItem 等方法。具体是 ListUpdateCallback
简单来说就是 调用了submitList 就没必要再去调用 notify 方法了
(2) PagedList
PageList继承AbstractList,支持所有List的操作。同时它还有一个重要的成员变量,PagedStorage。PagedStorage 有如下变量
private final ArrayList<List<T>> mPages;
说明是按页存储数据。PagedList会从Datasource中加载数据,更准确的说是通过Datasource加载数据, 通过Config的配置,可以设置一次加载的数量以及预加载的数量。 除此之外,PagedList还可以向RecyclerView.Adapter发送更新的信号,驱动UI的刷新。
PagedList 有三个子类ContiguousPagedList,SnapshotPagedList,TiledPagedList。可以通过 create 方法找到
static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
@NonNull Executor notifyExecutor,
@NonNull Executor fetchExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
@Nullable K key) {
if (dataSource.isContiguous() || !config.enablePlaceholders) {
......
ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
return new ContiguousPagedList<>(***);
} else {
return new TiledPagedList<>((PositionalDataSource<T>) dataSource,);
}
}
SnapshotPagedList 这个暂时可以不管,类似于弄了一个副本。ContiguousPagedList和TiledPagedList之后再介绍
(3)DataSource
DataSource<Key, Value>从字面意思理解是一个数据源,其中key对应加载数据的条件信息,Value对应加载数据的实体类。
DataSource是一个抽象类,但是我们不能直接继承它实现它的子类。但是Paging库里提供了好些它的子类
DataSource --- ContiguousDataSource --- ItemKeyedDataSource --- WrapperItemKeyedDataSource
DataSource --- ContiguousDataSource --- PageKeyedDataSource --- WrapperPageKeyedDataSource
DataSource --- PositionalDataSource --- ListDataSource
DataSource --- PositionalDataSource --- WrapperPositionalDataSource
- PageKeyedDataSource<Key, Value>:适用于目标数据根据页信息请求数据的场景,即Key 字段是页相关的信息。比如请求的数据的参数中包含类似next/previous页数的信息。
- ItemKeyedDataSource<Key, Value>:适用于目标数据的加载依赖特定item的信息, 即Key字段包含的是Item中的信息,比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的ID时,该场景多出现于论坛类应用评论信息的请求。
- PositionalDataSource<T>:适用于目标数据总数固定,通过特定的位置加载数据,这里Key是Integer类型的位置信息,T即Value。 比如从数据库中的1200条开始加在20条数据。
还有其他的,比如 ListDataSource ,其实就是已经定制好的,可以直接用的
(4) PageKeyedDataSource 和 ContiguousPagedList
一般的网络请求都是分页的,所以我把这个单独拿出来分析一下。
我们从 ContiguousPagedList 的 loadAroundInternal 开始
protected void loadAroundInternal(int index) {
int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index,
mStorage.getLeadingNullCount());
int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index,
mStorage.getLeadingNullCount() + mStorage.getStorageCount());
mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
if (mPrependItemsRequested > 0) {
schedulePrepend();
}
mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
if (mAppendItemsRequested > 0) {
scheduleAppend();
}
}
private void scheduleAppend() {
if (mAppendWorkerState != READY_TO_FETCH) {
return;
}
mAppendWorkerState = FETCHING;
final int position = mStorage.getLeadingNullCount()
+ mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();
// safe to access first item here - mStorage can't be empty if we're appending
final V item = mStorage.getLastLoadedItem();
mBackgroundThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (isDetached()) {
return;
}
if (mDataSource.isInvalid()) {
detach();
} else {
mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
mMainThreadExecutor, mReceiver);
}
}
});
}
走 mDataSource.dispatchLoadAfter 方法
final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
@Nullable Key key = getNextKey();
if (key != null) {
loadAfter(new LoadParams<>(key, pageSize),
new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
} else {
receiver.onPageResult(PageResult.APPEND, PageResult.<Value>getEmptyResult());
}
}
如果设置了key,就自己实现loadAfter。如果没设置,就用 ContiguousPagedList 默认的 mReceiver。在里面可以看到 mStorage.appendPage
2.自己动手实现一个 paging demo
首先我们来简单看一下Paging库的工作示意图,主要是分为如下几个步骤
- 使用DataSource从服务器获取或者从本地数据库获取数据(需要自己实现)
- 将数据保存到PageList中(会根据DataSource类型来生成对应的PageList,paging库已实现)
- 将PageList的数据submitList给PageListAdapter(需要自己调用)
- PageListAdapter在后台线程对比原来的PageList和新的PageList,生成新PageList(Paging库已实现对比操作,用户只需提供DiffUtil.ItemCallback实现)
PageListAdapter通知RecyclerView更新
(1)使用DataSource从服务器获取数据
这里我们就用官方demo的url做测试。因为是分页加载的,所以肯定选用PageKeyedDataSource
public class UserPageKeyedDataSource extends PageKeyedDataSource<String, Repo> {
private int pageNum = 0;
private GithubService service;
UserPageKeyedDataSource(GithubService service) {
this.service = service;
}
@Override
public void loadInitial(@NonNull LoadInitialParams<String> params, @NonNull LoadInitialCallback<String, Repo> callback) {
pageNum = 0;
try {
Response<RepoSearchResponse> response = service.searchRepos("Android" + IN_QUALIFIER, pageNum, 20).execute();
callback.onResult(response.body().getItems(), "", "");
} catch (Exception e) {
}
}
/**
* 请求上一页数据(基本不用)
*/
@Override
public void loadBefore(@NonNull LoadParams<String> params, @NonNull LoadCallback<String, Repo> callback) {
}
/**
* 请求下一页数据
*/
@Override
public void loadAfter(@NonNull LoadParams<String> params, @NonNull LoadCallback<String, Repo> callback) {
pageNum++;
try {
Response<RepoSearchResponse> response = service.searchRepos("Android" + IN_QUALIFIER, pageNum, 20).execute();
callback.onResult(response.body().getItems(), "");
} catch (Exception e) {
}
}
}
用到的相关类贴出来
const val IN_QUALIFIER = "in:name,description"
/**
* Github API communication setup via Retrofit.
*/
interface GithubService {
/**
* Get repos ordered by stars.
*/
@GET("search/repositories?sort=stars")
fun searchRepos(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") itemsPerPage: Int
): Call<RepoSearchResponse>
companion object {
private const val BASE_URL = "https://api.github.com/"
fun create(): GithubService {
val logger = HttpLoggingInterceptor()
logger.level = Level.BASIC
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
//.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(IoScheduler()))
.build()
.create(GithubService::class.java)
}
}
}
data class RepoSearchResponse(
@SerializedName("total_count") val total: Int = 0,
@SerializedName("items") val items: List<Repo> = emptyList(),
val nextPage: Int? = null
)
data class Repo(
@field:SerializedName("id") val id: Long,
@field:SerializedName("name") val name: String,
@field:SerializedName("full_name") val fullName: String,
@field:SerializedName("description") val description: String?,
@field:SerializedName("html_url") val url: String,
@field:SerializedName("stargazers_count") val stars: Int,
@field:SerializedName("forks_count") val forks: Int,
@field:SerializedName("language") val language: String?
)
(2)配置PageList
PageList主要负责控制 第一次默认加载多少数据,之后每一次加载多少数据,如何加载 等等。同时将数据的变更反映到UI上。
val adapter = MyPagedAdapter()
recycleview.layoutManager = LinearLayoutManager(this)
recycleview.adapter = adapter
val sourceFactory = UserDataSourceFactory(GithubService.create())
LivePagedListBuilder(sourceFactory, 20)
//.setFetchExecutor()
.build().observe(this,
Observer<PagedList<Repo>> {
adapter.submitList(it)
})
这里我们用LiveData来观察。注意她有个参数是 DataSource.Factory。这是DataSource 的内部工厂类,通过create()方法就可以获得DataSource 的实例。
public class UserDataSourceFactory extends DataSource.Factory<String, Repo> {
private GithubService service;
UserDataSourceFactory(GithubService service) {
this.service = service;
}
@NonNull
@Override
public DataSource<String, Repo> create() {
return new UserPageKeyedDataSource(service);
}
}
(3)配置adapter
public class MyPagedAdapter extends PagedListAdapter<Repo, MyPagedAdapter.UserHolder> {
public MyPagedAdapter() {
super(callback);
}
@NonNull
@Override
public MyPagedAdapter.UserHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new UserHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout, parent, false));
}
@Override
public void onBindViewHolder(@NonNull MyPagedAdapter.UserHolder holder, int position) {
holder.bind(getItem(position));
}
static class UserHolder extends RecyclerView.ViewHolder {
TextView mText;
public UserHolder(@NonNull View itemView) {
super(itemView);
mText = itemView.findViewById(R.id.txt);
}
public void bind(Repo repo) {
mText.setText(repo.getFullName());
}
}
/**
* DiffCallback的接口实现中定义比较的规则,比较的工作则是由PagedStorageDiffHelper来完成
*/
private static final DiffUtil.ItemCallback<Repo> callback = new DiffUtil.ItemCallback<Repo>() {
@Override
public boolean areItemsTheSame(@NonNull Repo oldItem, @NonNull Repo newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull Repo oldItem, @NonNull Repo newItem) {
return oldItem.getFullName().equals(newItem.getFullName());
}
};
}
3.参考
Android Paging library详解(一)
Android Paging library详解(二)
Android Paging