1、Paging库介绍
这里就不详细介绍、官网写的非常详细清楚:
传送门:库优势 库架构 基本原理
本文是针对Java向的开发,想学习Kotlin的可以参考郭霖这篇博文:
2、Paging库基本用法(结合RxJava)
1)导入依赖:
implementation "androidx.paging:paging-runtime:3.0.1"
//使用RxJava,需要引入这个依赖 (RxJava2、RxJava3均可,这里以RxJava2为例)
implementation "androidx.paging:paging-rxjava2:3.0.1"
这里的版本号对自己项目minSdkVersion是有要求的,可以根据实际情况进行修改。
使用RxJava 还需要如下依赖:
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.9'
2)构建RxPagingSource(kotlin是直接构建PagingSource)
RxPagingSource<Integer, Bean> :
Integer | 页数数据类型 |
Bean | 列表Item项的实体类型 |
实现loadSingle方法:
@Override
public @NotNull Single<LoadResult<Integer, Bean>> loadSingle(@NotNull LoadParams<Integer> loadParams) {
Integer nextPageNumber = loadParams.getKey();
if (nextPageNumber == null) {
nextPageNumber = 1;
}
Integer finalPage = nextPageNumber;
int loadSize = loadParams.getLoadSize();
return Single.fromCallable(() -> repository
.getNeedPagingList(type, loadSize, loadSize * (finalPage - 1)))
.subscribeOn(Schedulers.io()).map(new Function<List<Bean>, LoadResult<Integer, Bean>>() {
@Override
public LoadResult<Integer, Bean> apply(@NonNull List<Bean> mBeans) throws Exception {
return toLoadResult(mBeans, finalPage);
}
}).onErrorReturn(new Function<Throwable, LoadResult<Integer, Bean>>() {
@Override
public LoadResult<Integer, Bean> apply(@NonNull Throwable throwable) throws Exception {
return new LoadResult.Error<>(throwable);
}
});
}
/**
* 功能描述 将获取的集合对象转化为需加载的结果对象
*
* @param mBeans 待加载的实体
* @param page 对应的页数
* @return: androidx.paging.PagingSource.LoadResult<java.lang.Integer, com.xxx.xxx.Bean>
* @since 1.0
*/
private LoadResult<Integer, Bean> toLoadResult(@NonNull List<Bean> mBeans, Integer page) {
Integer prevKey = page == 1 ? null : page - 1;
Integer nextKey = mBeans.isEmpty() ? null : page + 1;
return new LoadResult.Page<>(mBeans, prevKey, nextKey, LoadResult.Page.COUNT_UNDEFINED,
LoadResult.Page.COUNT_UNDEFINED);
}
loadSingle | RxJava对应的方法,Kotlin是对应load方法 |
loadParams | getkey:代表当前页数,可为null. |
LoadResult.Page() | prevKey:当前页的上一页页数 (如当前第一页,没有上一页,所以上一页为null) |
这里有个getRefreshKey方法 涉及高级用法,可实现后放着,返回null就行。
3)设置 PagingData 流
我这里是在ViewModel里实现的:
/**
*
*功能描述 获取PagingData流
* @param type 类型,(假如有搜索功能的话,可作为列表一个过滤条件)
* @return: io.reactivex.Flowable<androidx.paging.PagingData < com.xxx.xxx.Bean>>
*@since 1.0
*/
public Flowable<PagingData<Bean>> getPagingData(String type) {
CoroutineScope viewmodelScope = ViewModelKt.getViewModelScope(this);
Pager<Integer, Bean> pager = new Pager<Integer, Bean>(new PagingConfig(60, 10, true, 60),
() -> new MyPagingSource(type,repository));
Flowable<PagingData<Bean>> flowable = PagingRx.getFlowable(pager);
return PagingRx.cachedIn(flowable, viewmodelScope);
}
PagingConfig:
PageSize | 一页加载的项目数 |
prefetchDistance | 预取数,意思提前多少项目数拉取下一屏数据,例如设置为10的话,当前加载了60条数据,当用户滑倒第50条时候,就会提前加载下一屏数据,注意这个值必须大于0。(Paging列表丝滑的原因之一) |
enablePlaceholders | 是否启用占位符,启用的话,item会先填充默认的布局,待数据获取加载后,再刷新item,(Paging列表丝滑的原因之一) |
initlalLoadSize | 初始化第一次加载item数。 |
cachedIn():
作用是在viewModelScope这个作用域内进行缓存,假如手机横竖屏发生了旋转导致了Activity重新创建,Paging3就可以直接读取缓存中的数据,而不会重新去发起网络请求了(节省带宽资源)。
4)定义 RecyclerView 适配器
自定义PagingDataAdapter,其基本用法和RecyclerView.Adapter一致,都需要实现onCreateViewHolder,ViewHolder,onBindViewHolder,getItemCount,下面重点说需要注意的地方:
public PagingListAdapter(MyComparator myComparator, Context mContext) {
super(myComparator);
this.mContext = mContext;
}
public static class MyComparator extends DiffUtil.ItemCallback<Bean> {
@Override
public boolean areItemsTheSame(@NonNull Bean oldItem, @NonNull Bean newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull Bean oldItem, @NonNull Bean newItem) {
return oldItem.getInfo().equals(newItem.getInfo());
}
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
Bean mBean =getItem(position);
if (mBean == null) {
return;
}
holder.tvInfo.setText(mBean.getInfo());
...
}
@Override
public int getItemCount() {
return super.getItemCount();
}
首先是需要实现DiffUtil.ItemCallback<?> 这个方法是也是Paging能够实现去重功能的关键,从而能够更高效的利用网络带宽和系统资源,大家也不想每次刷新、拉取都重新去加载旧数据是吧。
然后说下onBindViewHolder里获取item项的方法getItem(position);这个方法是固定的,不要误把列表数据集合又传到这个apdater里面了,咱们的数据流是通过上述PagingData形式传递给Paging了,Paging会通过其内部机制自主的管理数据如何分页、加载等。
getItemCount()也是一样,固定写法。
5)显示列表。
PagingListAdapter adapter = new PagingListAdapter(new PagingListAdapter.MyComparator(), getContext());
mViewModel.getPagingData(type).as(RxLife.asOnMain(getViewLifecycleOwner())).subscribe(mBeanPagingData -> {
adapter.submitData(getLifecycle(), mBeanPagingData);
});
就这么简单,当执行submitData后,Paging的整个机制就运转起来了,等着界面显示即可。
通过以上步骤,基本实现了一个简单Paging列表。但是这个列表会有个问题,就是不管是开始加载还是上拉加载更多,列表都没加载状态的显示效果,这样在数据量较大或网络不太稳定情况下,用户体验感会很差,那么接下来内容就是给Paging列表添加状态效果,包括加载完成后,在列表尾部显示加载完毕等文字效果。
3、Paging3列表显示加载状态。
1)启动列表显示加载状态。
实现addLoadStateListener监听:
adapter.addLoadStateListener(new Function1<CombinedLoadStates, Unit>() {
@Override
public Unit invoke(CombinedLoadStates combinedLoadStates) {
LoadState loadState = combinedLoadStates.getRefresh();
if (loadState instanceof LoadState.NotLoading) {
//关闭进度条
Alert.progressClose();
} else if (loadState instanceof LoadState.Loading) {
//显示进度条
Alert.progresShow(getContext(), getContext().getString(R.string.review_progress_text));
} else if (loadState instanceof LoadState.Error) {
LoadState.Error error = (LoadState.Error) loadState;
//关闭进度条,并弹出错误原因吐司
Alert.progressClose();
Toast.makeText(getContext(), "Error:" + error.getError().getMessage(), Toast.LENGTH_SHORT).show();
}
return null;
}
});
getRefresh | 获取初始化刷新时加载状态 |
getAppend | 在加载更多的时候的加载状态 |
getPrepend | 在当前列表头部添加数据时的加载状态 |
NotLoading | 没加载,无错误 |
Loading | 数据加载中 |
Error | 加载出错 |
2)加载更多时状态加载
实现LoadStateAdapter:
public class PagingLoadStateAdapter extends LoadStateAdapter<PagingLoadStateAdapter.MyViewHolder> {
private final Context context;
private final PagingListAdapter adapter;
public PagingLoadStateAdapter(Context context,PagingListAdapter adapter) {
this.context = context;
this.adapter=adapter;
}
@Override
public void onBindViewHolder(@NotNull PagingLoadStateAdapter.MyViewHolder myViewHolder,
@NotNull LoadState loadState) {
myViewHolder.bind(loadState,adapter);
}
@NotNull
@Override
public PagingLoadStateAdapter.MyViewHolder onCreateViewHolder(@NotNull ViewGroup viewGroup,
@NotNull LoadState loadState) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.load_state_item, viewGroup, false);
return new MyViewHolder(view);
}
public static class MyViewHolder extends RecyclerView.ViewHolder {
private final TextView mErrorMsg;
private final TextView mStatus;
private final Button mRetry;
private final ProgressBar progressBar;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
mErrorMsg = itemView.findViewById(R.id.errorMsg);
mRetry = itemView.findViewById(R.id.retryButton);
progressBar = itemView.findViewById(R.id.progressBar);
mStatus = itemView.findViewById(R.id.tv_stats);
}
/**
* 功能描述 加载状态显示
*
* @param loadState 加载状态
* @since 1.0
*/
public void bind(LoadState loadState) {
if (loadState instanceof LoadState.NotLoading) {
if (loadState.getEndOfPaginationReached()) {
progressBar.setVisibility(View.GONE);
mStatus.setVisibility(View.VISIBLE);
mStatus.setText("数据加载完毕");
mRetry.setVisibility(View.GONE);
mErrorMsg.setVisibility(View.GONE);
}
}
if (loadState instanceof LoadState.Loading) {
progressBar.setVisibility(View.VISIBLE);
} else {
progressBar.setVisibility(View.GONE);
}
if (loadState instanceof LoadState.Error) {
mErrorMsg.setVisibility(View.VISIBLE);
mRetry.setVisibility(View.VISIBLE);
mRetry.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
adapter.retry();
}
});
mStatus.setVisibility(View.GONE);
LoadState.Error loadStateError = (LoadState.Error) loadState;
mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
} else {
mErrorMsg.setVisibility(View.GONE);
mRetry.setVisibility(View.GONE);
}
}
}
@Override
public boolean displayLoadStateAsItem(@NotNull LoadState loadState) {
return (loadState instanceof LoadState.Loading || loadState instanceof LoadState.Error || loadState instanceof LoadState.NotLoading && loadState
.getEndOfPaginationReached());
}
}
设置LoadStateAdapter:
PagingLoadStateAdapter pagingLoadStateAdapter = new PagingLoadStateAdapter(getActivity(),adapter);
ConcatAdapter concatAdapter = adapter.withLoadStateFooter(pagingLoadStateAdapter);
withLoadStateFooter | 当前列表尾部添加 |
withLoadStateHeader | 当前列表头部添加 |
withLoadStateHeaderAndFooter | 当前列表头部和尾部分别添加 |
这里loadState状态可以参考启动列表的加载状态,含义是一样的,这里就不多赘述。
这里需要注意的是:
PagingDataAdapter一定要先设置withLoadStateFooter,然后返回一个新的concatAdapter对象,再把这个对象赋值给RecyclerView进行setAdapter,才会生效。
重写displayLoadStateAsItem方法,重写的原因就是为了在用户列表滑到底部时候显示数据加载完毕这个文字。查看源码可以看到displayLoadStateAsItem默认只是显示LoadState.Loading 和LoadState.Error,所以假如不重写就会发现滑到底部后,文字仅仅是一闪而逝,而不会停留在底部。
重试的话,也很简单,直接调用PagingDataAdapter的retry()即可。
好的,到这里基本上实现了Paging的启动加载,状态显示,上拉加载更多,重试等,现在就剩下下拉刷新
4、Paging3结合swiperefreshlayout实现下拉刷新功能。
看过paging库介绍,是个分页加载库,它封装了一套完整的分页加载流程,虽然其自带刷新功能,但是并没有自带的刷新界面的动画效果,因此需要结合swiperefreshlayout这个库配合Paging使用来实现下拉刷新功能。
1)swiperefreshlayout依赖导入
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
2)将swiperefreshlayout嵌套在RecyclerView上
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swiperefreshlayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="@dimen/fragment_margin_left"
android:layout_marginEnd="@dimen/fragment_margin_right"
app:layout_constraintBottom_toTopOf="@+id/guideline19"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline18">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/line_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/track_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/line_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/track_drawable" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
3)实现swiperefreshlayout刷新监听调用Paging3的刷新逻辑。
swiperefreshlayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
adapter.refresh();
}
});
4)配合Paging3刷新时加载状态,显示关闭swiperefreshlayout。
adapter.addLoadStateListener(new Function1<CombinedLoadStates, Unit>() {
@Override
public Unit invoke(CombinedLoadStates combinedLoadStates) {
LoadState loadState = combinedLoadStates.getRefresh();
if (loadState instanceof LoadState.NotLoading) {
swiperefreshlayout.setRefreshing(false);
} else if (loadState instanceof LoadState.Loading) {
swiperefreshlayout.setRefreshing(true);
} else if (loadState instanceof LoadState.Error) {
swiperefreshlayout.setRefreshing(false);
LoadState.Error error = (LoadState.Error) loadState;
Toast.makeText(getContext(), "Error:" + error.getError().getMessage(), Toast.LENGTH_SHORT).show();
}
return null;
}
});
至此大功告成!