Android-Jetpack笔记-Paging结合网络数据

上篇文章介绍了paging+room的使用,这篇主要介绍paging+网络数据的使用和原理。

Jetpack笔记代码

本文源码基于SDK 29

使用

网络数据来源于玩Android开放API,运行效果:

引入依赖:

1
2
def paging_version = "2.1.1"
implementation "androidx.paging:paging-runtime:$paging_version"

创建一个ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//PagingNetworkViewModel2.java
LiveData<PagedList<ArticleBean.DataBean.Article>> mPageData;
DataSource mDataSource; //数据源

//数据源工厂
private DataSource.Factory mFactory = new DataSource.Factory() {
@Override
public DataSource create() {
if (mDataSource == null || mDataSource.isInvalid()) {
//下拉刷新调用mDataSource.invalidate(),这时需要创建一个新的数据源
mDataSource = createDataSource();
}
return mDataSource;
}
};

//创建数据源
private DataSource createDataSource() {
return new ItemKeyedDataSource<Integer, ArticleBean.DataBean.Article>() {

//paging首次加载数据
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<ArticleBean.DataBean.Article> callback) {
//这3个load方法在子线程中执行,同步获取网络数据即可
callback.onResult(Api.getArticle(String.valueOf(mPage++)));
}

//paging加载更多数据,在滑动到配置好的位置时,自动触发
@Override
public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<ArticleBean.DataBean.Article> callback) {
callback.onResult(Api.getArticle(String.valueOf(mPage++)));
}

@Override
public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<ArticleBean.DataBean.Article> callback) {
//向前加载,忽略即可
}

@Override
public Integer getKey(@NonNull ArticleBean.DataBean.Article item) {
return item.getId();
}
};
}

public LiveData<PagedList<ArticleBean.DataBean.Article>> getPageData() {
if (null == mPageData) {
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(20) //分页大小
.setInitialLoadSizeHint(20) //首次加载大小
.setPrefetchDistance(10) //预加载距离:还剩10个就要滑到底了,就进行预加载
.build();
mPageData = new LivePagedListBuilder(mFactory, config).build();
}
return mPageData;
}

//下拉刷新
public void refresh() {
mPage = 0;
mDataSource.invalidate();
}

创建适配器NetworkListAdapter2继承自PagedListAdapter,这里只需提供一个数据diff的规则DiffUtil.ItemCallback即可,onCreateViewHolderonBindViewHolder的用法和老的RecyclerView.Adapter没区别已省略,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//NetworkListAdapter2.java
NetworkListAdapter2() {
super(new DiffUtil.ItemCallback<ArticleBean.DataBean.Article>() {
@Override
public boolean areItemsTheSame(@NonNull ArticleBean.DataBean.Article oldItem, @NonNull ArticleBean.DataBean.Article newItem) {
return oldItem.getId() == newItem.getId();
}

@Override
public boolean areContentsTheSame(@NonNull ArticleBean.DataBean.Article oldItem, @NonNull ArticleBean.DataBean.Article newItem) {
return oldItem.equals(newItem);
}
});
}

在activity中使用,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//PagingNetworkActivity2.java
onCreate(Bundle savedInstanceState) {
//关闭加载更多,使用paging的预加载即可
mBinding.refreshArticle.setEnableLoadMore(false);
mBinding.refreshArticle.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh(@NonNull RefreshLayout refreshLayout) {
//下拉刷新
mViewModel.refresh();
}
});
mAdapter = new NetworkListAdapter2();
mBinding.rvArticle.setAdapter(mAdapter);
mBinding.rvArticle.setLayoutManager(new LinearLayoutManager(this));

mViewModel.getPageData().observe(this, new Observer<PagedList<ArticleBean.DataBean.Article>>() {
@Override
public void onChanged(PagedList<ArticleBean.DataBean.Article> articles) {
mAdapter.submitList(articles);
}
});
}

这几个类名都加了后缀2,这是因为笔者先写了一套老的RecyclerView.Adapter使用方案,用来对比两套实现方案,代码见Jetpack笔记代码,欢迎star。

原理

同样,还是选择了几个问题进行分析,因为带着问题去跟进才能更聚焦:

  • 预加载怎么触发加载更多的
  • mDataSource.invalidate()怎么实现下拉刷新的

预加载怎么触发加载更多的

首先来到PagingNetworkViewModel2里创建数据源的代码,在loadAfter方法里打个断点,上滑加载更多,查看调用栈,

因为切了线程,调用栈不是很全,点下第三行来到这里,

1
2
3
4
5
6
7
8
9
10
11
12
//ContiguousPagedList.java
@MainThread
private void scheduleAppend() {
mLoadStateManager.setState(LoadType.END, LoadState.LOADING, null);
mBackgroundThreadExecutor.execute(new Runnable() {
@Override
public void run() {
mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
mMainThreadExecutor, mReceiver);
}
});
}

给这个方法的第一行打一个断点,再触发一次加载更多,看看哪里调了scheduleAppend

这时调用链就很清晰了,在onBindViewHolder中我们调了getItem取出条目数据,进而触发预加载的逻辑。下面顺着这个链路跟进看看,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//PagedListAdapter.java
protected T getItem(int position) {
return mDiffer.getItem(position);
}

//AsyncPagedListDiffer.java
public T getItem(int index) {
mPagedList.loadAround(index);
}

//PagedList.java
public void loadAround(int index) {
loadAroundInternal(index);
}

//ContiguousPagedList.java
protected void loadAroundInternal(int index) {
scheduleAppend();
}

private void scheduleAppend() {
//这里切到了子线程
mBackgroundThreadExecutor.execute(new Runnable() {
@Override
public void run() {
mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
mMainThreadExecutor, mReceiver);
}
});
}

//ItemKeyedDataSource.java
final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
//子线程回调
loadAfter(new LoadParams<>(getKey(currentEndItem), pageSize),
new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
}

然后在子线程回调的方法loadAfter里,我们同步获取网络数据,

1
2
3
4
5
6
7
8
9
10
//PagingNetworkViewModel2.java
private DataSource createDataSource() {
return new ItemKeyedDataSource<Integer, ArticleBean.DataBean.Article>() {
@Override
public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<ArticleBean.DataBean.Article> callback) {
//同步获取网络数据,然后callback.onResult
callback.onResult(Api.getArticle(String.valueOf(mPage++)));
}
}
}

callback.onResult继续分发,链条有点长,就直接列出来了,

ItemKeyedDataSource.LoadCallbackImpl#onResult
DataSource.LoadCallbackHelper#dispatchToReceiver(这里切回了主线程)
DataSource.LoadCallbackHelper#dispatchOnCurrentThread
PageResult.Receiver#onPageResult
PagedStorage#appendPage
ContiguousPagedList#onPageAppended
PagedList#notifyInserted
PagedList.Callback#onInserted(在AsyncPagedListDiffer类里)
AdapterListUpdateCallback#onInserted(这里调了mAdapter.notifyItemRangeInserted添加新数据)

mDataSource.invalidate()怎么实现下拉刷新的

首先我们会调用mDataSource.invalidate(),然后来到,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//DataSource.java
public void invalidate() {
if (mInvalid.compareAndSet(false, true)) {
for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
callback.onInvalidated();
}
}
}

//LivePagedListBuilder.java
final DataSource.InvalidatedCallback mCallback =
new DataSource.InvalidatedCallback() {
@Override
public void onInvalidated() {
invalidate();
}
};

//ComputableLiveData.java
public void invalidate() {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
//mInvalidationRunnable - mRefreshRunnable - compute()
}

//LivePagedListBuilder.java
protected PagedList<Value> compute() {
//调用我们提供的数据源工厂类实现,创建数据源
mDataSource = dataSourceFactory.create();
mList = new PagedList.Builder<>(mDataSource, config)
.setNotifyExecutor(notifyExecutor)
.setFetchExecutor(fetchExecutor)
.setBoundaryCallback(boundaryCallback)
.setInitialKey(initializeKey)
.build();
return mList;
}

//ComputableLiveData.java
//执行完compute,设置值,通知观察者
mLiveData.postValue(value);

//回到了activity
mViewModel.getPageData().observe(this, new Observer<PagedList<ArticleBean.DataBean.Article>>() {
@Override
public void onChanged(PagedList<ArticleBean.DataBean.Article> articles) {
//重新提交数据
mAdapter.submitList(articles);
}
});

回到我们的数据源工厂类实现,

1
2
3
4
5
6
7
8
9
10
11
//PagingNetworkViewModel2.java
private DataSource.Factory mFactory = new DataSource.Factory() {
@Override
public DataSource create() {
if (mDataSource == null || mDataSource.isInvalid()) {
//下拉刷新调用mDataSource.invalidate(),这时需要创建一个新的数据源
mDataSource = createDataSource();
}
return mDataSource;
}
};

mDataSource.invalidate()下拉刷新必须创建新的数据源,否则将引起死循环,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//LivePagedListBuilder.java
protected PagedList<Value> compute() {
do {
mDataSource = dataSourceFactory.create();
mDataSource.addInvalidatedCallback(mCallback);
mList = new PagedList.Builder<>(mDataSource, config)
.setNotifyExecutor(notifyExecutor)
.setFetchExecutor(fetchExecutor)
.setBoundaryCallback(boundaryCallback)
.setInitialKey(initializeKey)
.build();
} while (mList.isDetached());//始终为true,引起死循环
return mList;
}

优缺点

  • 优点:
    • 自带分页,预加载处理
    • 子线程diff,主线程局部刷新
    • 可以和Room无缝结合
  • 缺点:
    • 使用复杂,有待封装

参考文章