jetpack系列和UI相关的我已经发布过很多篇文章,其实,写文章的目的一方面是为了记录一些知识点,毕竟年纪大了,方便后面真正使用到的时候,能够快速上手;另一方面,也是将知识点分享给读者们,共同进步。说了那么多,其实我想说的是,这篇文章是基于之前的文章基础上,用到的知识点包括ViewModel,dataBinding,LiveData等,并且是基于这个项目基础上展开的ARoute+Rxjava2+Retrofit2+Okhttp+MVVM。代码中,你可以看到很多我没有贴出来代码的基类,其实都可以在原先的项目中看到的。建议先看看,之前的文章。开撸!!!
一、导入
app的build.gradle中添加如下配置:
implementation 'androidx.paging:paging-runtime:2.1.2'
二、使用
1、新建DataSource.Factory基类
对外提供createBuilder方法,得到LivePagedListBuilder,通过它才能构建出LiveData数据。
/**
* <pre>
* author : QB
* time : 2020/3/31
* version : v1.0.0
* desc : factory基类
* </pre>
*/
public abstract class BasePositionPageDataSourceFactory<D,M extends BaseModel> extends DataSource.Factory<Integer, D> {
private M model;
public BasePositionPageDataSourceFactory(M model) {
this.model = model;
}
public abstract PagedList.Config getConfig();
public M getModel(){
return model;
}
public LivePagedListBuilder<Integer, D> createBuilder() {
return new LivePagedListBuilder<>(this, getConfig());
}
}
注:
- 泛型D:数据。比如我项目中的数据是BaseRecyclerViewModel
public abstract class BaseRecyclerViewModel implements Serializable, IBaseRecyclerItemType {
public String tag;
//id标识
public ObservableField<String> id = new ObservableField<>();
@Override
public int getItemType() {
return Integer.MAX_VALUE;
}
}
- M层:model层。比如我项目中使用的是MainPageModel
public class MainPageModel extends BaseModel<PagedList<MainBean>> {
public MainPageModel() {
}
@Override
public void loadNetwork() {
super.loadNetwork();
}
}
2、新建PositionalDataSource基类
public class BasePositionPageDataSource<D> extends PositionalDataSource<D> {
private int page;
private PagedList.Config config;
public PagedList.Config getConfig() {
return config;
}
/**
* 计算显示到第几条数据
*/
protected int startPos;
public BasePositionPageDataSource(PagedList.Config config) {
this.config = config;
}
/**
* recyclerView第一次加载时自动调用
*
* @param params 包含当前加载的位置position、下一页加载的长度count
* @param callback 将数据回调给UI界面使用callback.onResult
*/
@Override
public void loadInitial(@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback<D> callback) {
page = 0;
startPos = computeInitialLoadPosition(params, this.config.pageSize);
}
/**
* 当用户滑动recyclerView到下一屏的时候自动调用,这里我们自动加载下一页的数据
*
* @param params 包含当前加载的位置position、下一页加载的长度count
* @param callback 将数据回调给UI界面使用callback.onResult
*/
@Override
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<D> callback) {
page++;
}
}
两个关键方法loadInitial和loadRange需要子类重写。
- loadInitial:初始加载方法,说白了就是刚进入界面就会进入该方法,进行第一次的数据请求。
- loadRange:达到设置的条件后,自动加载更多。比如,我设置每一次加载10条数据,当需要第11条数据时,就会自动触发该方法。
3、DataSource具体实现类
public class MainDataSource<M extends MainPageModel> extends BasePositionPageDataSource<BaseRecyclerViewModel> {
...
@Override
public void loadInitial(@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback<BaseRecyclerViewModel> callback) {
super.loadInitial(params, callback);
//网络请求
//model.loadNetwork();
List<BaseRecyclerViewModel> data = initData(startPos,getConfig().pageSize);
//更新ui
callback.onResult(data,startPos);
}
@Override
public void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback<BaseRecyclerViewModel> callback) {
super.loadRange(params, callback);
//网络请求
List<BaseRecyclerViewModel> data = initData(params.startPosition,params.loadSize);
//更新ui
callback.onResult(data);
}
private List<BaseRecyclerViewModel> initData(int startPos,int end){
List<BaseRecyclerViewModel> data = new ArrayList<>();
...
return data;
}
}
只提出了关键代码
- 在loadInitial方法中调用 callback.onResult(data,startPos) 通知界面更新数据,并告诉数据源我现在起始位置。
- 在loadRange方法中通过 callback.onResult(data) 通知更新界面
4、DataSource.Factory实现类
public class MainDataSourceFactory extends BasePositionPageDataSourceFactory<BaseRecyclerViewModel,MainPageModel> {
public MainDataSourceFactory(MainPageModel model) {
super(model);
}
@Override
public PagedList.Config getConfig() {
return new PagedList.Config.Builder()
.setPageSize(10)//每页加载个数
.setEnablePlaceholders(false)//是否启用占位符
.setPrefetchDistance(10)//距离底部多远,开始加载下一页数据
.setInitialLoadSizeHint(10)//初始化请求数据大小
.build();
}
@NonNull
@Override
public DataSource<Integer, BaseRecyclerViewModel> create() {
return new MainDataSource<>(getConfig(),getModel());
}
}
设置PagedList.Config属性,决定Paging数据加载格式。
5、布局
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:rvPage="http://schemas.android.com/apk/res-auto"
xmlns:sw="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.xinyartech.main.main.pageList.MainPageViewModel" />
<variable
name="adapter"
type="com.xinyartech.main.main.pageList.MainPageAdapter" />
<variable
name="dataList"
type="androidx.paging.PagedList<com.xinyartech.base.recyclerView.BaseRecyclerViewModel>" />
<variable
name="lifeCycleOwner"
type="androidx.lifecycle.LifecycleOwner" />
</data>
<LinearLayout
android:id="@+id/mainLin"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/sw"
android:layout_width="match_parent"
sw:refreshPageAdapter="@{adapter}"
sw:lifecycleOwner="@{lifeCycleOwner}"
sw:viewModel="@{viewModel}"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
rvPage:pageAdapter="@{adapter}"
rvPage:pageDataList="@{dataList}" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</layout>
我自定义了RecyclerView的BindAdapter,所有的逻辑都会在里面实现。后面再看
6、适配器
这里的适配器继承至PagedListAdapter,构造方法中需要定义数据差异规则,交给DiffUtil.ItemCallback实现。
public class MainDiffCallback extends DiffUtil.ItemCallback<BaseRecyclerViewModel> {
@Override
public boolean areItemsTheSame(@NonNull BaseRecyclerViewModel oldItem,
@NonNull BaseRecyclerViewModel newItem) {
return Objects.equals(oldItem.id.get(), newItem.id.get());
}
@Override
public boolean areContentsTheSame(@NonNull BaseRecyclerViewModel oldItem,
@NonNull BaseRecyclerViewModel newItem) {
Log.e("差异化对比>>>",oldItem.id.get()+" "+newItem.id.get());
return Objects.equals(oldItem.id.get(), newItem.id.get());
}
}
public class MainPageAdapter extends BaseRecyclerViewPagingAdapter<BaseRecyclerViewHolder, BaseRecyclerViewModel> {
MainPageAdapter() {
super(new MainDiffCallback());
}
}
...
public abstract class BaseRecyclerViewPagingAdapter<VH extends BaseRecyclerViewHolder,
VM extends BaseRecyclerViewModel> extends PagedListAdapter<VM, VH> {
protected BaseRecyclerViewPagingAdapter(@NonNull DiffUtil.ItemCallback<VM> diffCallback) {
super(diffCallback);
}
...
}
7、自定义RecycleView绑定
/**
* 针对分页数据加载
*/
@BindingAdapter(value = {"pageAdapter", "pageDataList"}, requireAll = false)
public static <V extends BaseRecyclerViewHolder, VM extends BaseRecyclerViewModel> void setPageAdapter(RecyclerView recyclerView,BaseRecyclerViewPagingAdapter<V, VM> adapter, PagedList<VM> dataList) {
if (adapter != null) {
recyclerView.addItemDecoration(new CommonDecoration(recyclerView.getContext(),
R.drawable.drawable_itemdecoration));
adapter.submitList(dataList);
recyclerView.setAdapter(adapter);
}
}
最终调用adapter.submitList将数据填充到列表中。
8、自定义下拉刷新SwipeRefreshLayout绑定
/**
* 下拉刷新
*/
@BindingAdapter(value = {"refreshPageAdapter", "lifecycleOwner",
"viewModel"}, requireAll =
false)
@SuppressWarnings("All")
public static <V extends BaseRecyclerViewHolder, VM extends BaseRecyclerViewModel> void setSwipeRefreshLayout(SwipeRefreshLayout swipeRefreshLayout, BaseRecyclerViewPagingAdapter<V, VM> adapter,
LifecycleOwner lifecycleOwner,
BaseViewModel<?, PagedList<VM>> viewModel) {
if (adapter != null) {
try {
Method setOnRefreshListener = swipeRefreshLayout.getClass().getMethod(
"setOnRefreshListener", SwipeRefreshLayout.OnRefreshListener.class);
Method proxyMethod = viewModel.getClass().getMethod("reLoad");
//创建一个代理
Object proxyObj =
Proxy.newProxyInstance(SwipeRefreshLayout.OnRefreshListener.class.getClassLoader(),
new Class[]{SwipeRefreshLayout.OnRefreshListener.class},
(proxy, method, args) -> {
if (method.getName().equals("onRefresh")) {
swipeRefreshLayout.setRefreshing(true);
//不要忘记,要先设置为null
adapter.submitList(null);
//这一句将会刷新 returnData
Object invoke = proxyMethod.invoke(viewModel, args);
//设置监听
viewModel.returnData.observe(lifecycleOwner, vms -> {
adapter.submitList(vms);
swipeRefreshLayout.setRefreshing(false);
});
return invoke;
}
return method.invoke(proxy, args);
});
setOnRefreshListener.invoke(swipeRefreshLayout, proxyObj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
看到这里,大家应该有个疑惑点:为什么要是用动态代理?
我这里通过动态代理拦截了onRefresh方法,其实,刷新的监听设置可以在activity中实现,如下:
dataBinding.sw.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
}
});
但是,我认为Activity不应该出现和业务有关的代码,所以,我使用动态代理。 另外需要知道的是,你必须要知道以下知识点:
- 刷新数据,需要重新对数据进行监听即重新执行DataSource的loadInitial方法。
- 刷新之前,必须要调用adapter.submitList(null),设置为null,否则会出现界面内容不更新的情况(其实数据是更新的)。
针对这两点,我们从源码的角度分析。
8.1 为什么下拉刷新需要重新注册监听
首先我们需要找到,loadInitial方法是在哪里进行调用的,之前又说到,DataSource数据源,顾名思义就是提供数据的
public abstract class DataSource<Key, Value> {
}
是一个抽象类,我们看一下它的实现类之一PositionalDataSource
public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
public static class LoadInitialParams {
public final int requestedStartPosition;
public final int requestedLoadSize;
public final int pageSize;
public final boolean placeholdersEnabled;
...
}
public static class LoadRangeParams {
public final int startPosition;
public final int loadSize;
...
}
...
public abstract void loadInitial(
@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback<T> callback);
@WorkerThread
public abstract void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback<T> callback);
...
这个类就是提供给外部继承的,这也看不出来怎么初始化的。那从哪里看呢?LivePagedListBuilder.build。
构建LivePagedListBuilder的时候,就需要传递一个DataSource对象,说明剩下的都交给LivePagedListBuilder了
public LiveData<PagedList<Value>> build() {
return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
}
private static <Key, Value> LiveData<PagedList<Value>> create(...) {
return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
@Nullable
private PagedList<Value> mList;
@Nullable
private DataSource<Key, Value> mDataSource;
private final DataSource.InvalidatedCallback mCallback =
...
@SuppressWarnings("unchecked") // for casting getLastKey to Key
@Override
protected PagedList<Value> compute() {
...
mList = new PagedList.Builder<>(mDataSource, config)
.setNotifyExecutor(notifyExecutor)
.setFetchExecutor(fetchExecutor)
.setBoundaryCallback(boundaryCallback)
.setInitialKey(initializeKey)
.build();
...
return mList;
}
}.getLiveData();
}
列表数据都保存在mList中,需要跟踪它如何初始化的。进入PagedList.Builder.build方法。
public PagedList<Value> build() {
...
return PagedList.create(
dataSource...);
}
static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
...) {
...
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
key,
lastLoad);
} else {
return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
(key != null) ? (Integer) key : 0);
}
}
到了这里,先暂停以下,做一下总结:
DataSource数据源对外提供两个方法,交给代理类LivePagedListBuilder,通过它的build方法创建出LiveData对象。其中LiveData包裹的数据是交给类PagedList.Builder.build创建出来的。最终的创建过程如下:
我们继续跟踪源码,既然我们使用的PositionalDataSource,那么进入else代码块。进入TiledPagedList的构造方法
TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
...) {
...
mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
pageSize, mMainThreadExecutor, mReceiver);
...
}
这里的mDataSource是PositionalDataSource,所有调用它的dispatchLoadInitial方法。
final void dispatchLoadInitial(boolean acceptCount,
int requestedStartPosition, int requestedLoadSize, int pageSize,
@NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
LoadInitialCallbackImpl<T> callback =
new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);
LoadInitialParams params = new LoadInitialParams(
requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
loadInitial(params, callback);
...
}
最终调用loadInitial方法。好了,找到了。那么开始回答标题的疑问
代码中可以看到 loadInitial 方法,最终是在构造方法中调用的,所以如果需要重新执行它,就必须重新创建一个该对象。追根溯源,就是需要重新LivePagedListBuilder对象。那么相对应的LiveData数据也会重新创建,所以就需要对新数据设置监听。
8.2 为什么下拉刷新之前需要设置为null
首选看一下设置和不设置的效果有何不同?
设置前:
设置后:
看到不同了吧,没设置之前,发现下拉刷新之后,界面内容没有完全更新。 那为什么会出现这样的问题,又得从源码层进行分析了。咱们就来看看submitList方法。
最终会跟踪到AsyncPagedListDiffer类中:
public void submitList(@Nullable final PagedList<T> pagedList,
@Nullable final Runnable commitCallback) {
...
final PagedList<T> previous = (mSnapshot != null) ? mSnapshot : mPagedList;
if (pagedList == null) {
int removedCount = getItemCount();
if (mPagedList != null) {
mPagedList.removeWeakCallback(mPagedListCallback);
mPagedList = null;
} else if (mSnapshot != null) {
mSnapshot = null;
}
// dispatch update callback after updating mPagedList/mSnapshot
mUpdateCallback.onRemoved(0, removedCount);
onCurrentListChanged(previous, null, commitCallback);
return;
}
//只有当mPagedList == null才进来
if (mPagedList == null && mSnapshot == null) {
// fast simple first insert
mPagedList = pagedList;
pagedList.addWeakCallback(null, mPagedListCallback);
// dispatch update callback after updating mPagedList/mSnapshot
mUpdateCallback.onInserted(0, pagedList.size());
onCurrentListChanged(null, pagedList, commitCallback);
return;
}
if (mPagedList != null) {
// first update scheduled on this list, so capture mPages as a snapshot, removing
// callbacks so we don't have resolve updates against a moving target
mPagedList.removeWeakCallback(mPagedListCallback);
mSnapshot = (PagedList<T>) mPagedList.snapshot();
mPagedList = null;
}
if (mSnapshot == null || mPagedList != null) {
throw new IllegalStateException("must be in snapshot state to diff");
}
//下面就是对比两个集合的数据,
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);
}
}
});
}
});
}
注意这个mPagedList 存储的就是上一次的数据。只有满足条件的时候,才会执行mUpdateCallback.onInserted
mPagedList == null
而这个onInserted最终实现类重新的时候,都会执行如下代码:
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
此时的 position = 0,count 就是数据的长度。
所以,这就是我们必须要调用 submitList(null) 的原因。