前两天想了解下Paging
使用,就去看了下官方给的Sample
,大致扫了眼后, XX 我个 ** ,这么复杂的,这么一大堆类,都是干嘛的,由于Kotlin
处于只写过demo
并没有大量实际用来开发的地步,看着有点不习惯,就想着去找博客看看别人咋说的,大致扫了眼后, XX 我个 ** ,我就想了解下怎么用的,怎么都在扯原理,而且还是固定的套路,第一步,给个半成品的demo
,贴几行代码还不全,然后PageList
的原理,我连用都不会,我管啥原理我,看不懂啊
于是,我决定,自己写个尽量简单的仿造官方sample的半成品,不扯细节,不扯原理,我也不管啥优点缺点,主要是我不会,说不清,扯不了,我就是想瞅瞅咋用的而已,会用了之后,我才能去管细节扩展,才能晓得用在项目中替换代价大不大,记录下,能帮到别人最好
由于我把自己github
设置成了private
,就导致没办法直接放链接了
废话说完,上代码,一个简单的只使用网络请求作为数据源的Demo
implementation "android.arch.paging:runtime:2.1.0"
使用的 2.1.0
版本
类还是挺多的,用了wan android 的接口api
,就用了wan
,数据是以page
的方式,所以理所当然使用了Paging
的PageKeyedDataSource
来获取数据
重点有两个东西WanDataSource
和WanVm
,但是需要提前知道LiveData和ViewModel
的基本用法
1.0 布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".module.widget.paging.PagingActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/paging_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--Loading-->
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/paging_pb"
style="?android:attr/progressBarStyleLarge"
android:layout_width="60dp"
android:layout_height="60dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
超简单,就一个RecyclerView
一个ProgressBar
底部 Footer 布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tool="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/item_wan_footer_pb"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="10dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/item_wan_footer_tv_msg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tool:text="Something is error"
tool:visibility="visible" />
<Button
android:id="@+id/item_wan_footer_bt_retry"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:text="@string/wan_footer_retry"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/item_wan_footer_tv_msg"
tool:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
RecyclerView
的item
布局不再贴出来
2.0 Actitiy
public class PagingActivity extends BaseActivity {
private static final String TAG = PagingActivity.class.getSimpleName();
public static final String LOAD_TAG = "wan";
private WanAdapter mAdapter;
private ContentLoadingProgressBar mPb;
private boolean mIsLoadInitial = true;
private Runnable mRetryAction;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_paging);
initView();
initProvider();
}
@Override
protected void onStop() {
super.onStop();
OkHttpUtils.getInstance().cancelByTag(LOAD_TAG);
}
private void initProvider() {
ViewModelProvider provider = ViewModelProviders.of(this);
WanVm wanVm = provider.get(WanVm.class);
wanVm.errorMsgLiveData().observe(this, mAdapter::setErrorMsg);
wanVm.retryLiveData().observe(this, action -> mRetryAction = action);
wanVm.networkStateLiveData().observe(this, mAdapter::setNetworkState);
PagedList.Config pagedListConfig = new PagedList.Config.Builder()
// 分页加载的数量
.setPageSize(20)
// 是否启动PlaceHolders
.setEnablePlaceholders(false)
.build();
LiveData<PagedList<WanItem.DataBean.DatasBean>> wanLiveData =
new LivePagedListBuilder<>(new WanSourceFactory(wanVm), pagedListConfig).build();
wanLiveData.observe(this, list -> {
mAdapter.submitList(list);
if (mIsLoadInitial) {
mPb.setVisibility(View.GONE);
mIsLoadInitial = false;
Log.e(TAG, "-->" + list.size());
}
});
}
private void initView() {
mPb = $(R.id.paging_pb);
RecyclerView rv = $(R.id.paging_rv);
rv.addItemDecoration(new DividerItemDecoration(this, RecyclerView.VERTICAL));
mAdapter = new WanAdapter(new WanDiffCallback());
// Footer 布局内,发生错误,重试按钮
mAdapter.addRetryListener(() -> {
if (mRetryAction == null) {
return;
}
// new Thread(mRetryAction).start();
WorkerRunner.findRunner(PagingActivity.this).execute(mRetryAction);
});
rv.setAdapter(mAdapter);
}
}
initView()
就是初始化RecyclerView
,和不使用Paging
一样的套路,只是提供数据的方式需要变
重点在于initProvider()
方法内,首先是创建了WanVm
,关联了接收到具体消息时回调操作
PagedList.Config
,这玩意就是DataSource
分页数据的配置
LiveData<PagedList<WanItem.DataBean.DatasBean>> wanLiveData =
new LivePagedListBuilder<>(new WanSourceFactory(wanVm), pagedListConfig).build()
创建了一个LiveData
,用于获取WanDataSource
网络请求拿到的结果
2.1 WanAdapter
public class WanAdapter extends PagedListAdapter<WanItem.DataBean.DatasBean, RecyclerView.ViewHolder> {
public static final String TAG = WanAdapter.class.getSimpleName();
private NetworkState mNetworkState;
private OnRetryListener mRetryListener;
private String mErrorMsg;
public WanAdapter(@NonNull DiffUtil.ItemCallback<WanItem.DataBean.DatasBean> diffCallback) {
super(diffCallback);
mNetworkState = NetworkState.create();
}
public void addRetryListener(@NonNull OnRetryListener listener) {
mRetryListener = listener;
}
public void setErrorMsg(@NonNull String msg) {
this.mErrorMsg = msg;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == R.layout.item_wan_footer_layout) {
View item = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_wan_footer_layout, parent, false);
return new FooterHolder(item, mRetryListener);
} else {
View item = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_wan_layout, parent, false);
return new WanHolder(item);
}
}
@Override
public int getItemViewType(int position) {
if (hasExtraRow() && position == getItemCount() - 1) {
// loading
return R.layout.item_wan_footer_layout;
} else {
return R.layout.item_wan_layout;
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (footer(holder)) {
return;
}
if (holder instanceof WanHolder) {
WanHolder wanHolder = (WanHolder) holder;
WanItem.DataBean.DatasBean wan = getItem(position);
if (wan == null) {
return;
}
Context context = wanHolder.itemView.getContext();
// 标题
wanHolder.tvTitle.setText(wan.getTitle());
// 时间
wanHolder.tvTime.setText(wan.getNiceDate());
// 来自
String uer = TextUtils.isEmpty(wan.getAuthor()) ? wan.getShareUser() : wan.getAuthor();
String from = context.getResources().getString(R.string.wan_item_from, uer);
wanHolder.tvUser.setText(from);
// 分类
String cls = wan.getSuperChapterName() + "/" + wan.getChapterName();
String classification = context.getResources().getString(R.string.wan_item_class, cls);
wanHolder.tvClass.setText(classification);
}
}
private boolean footer(@NonNull RecyclerView.ViewHolder holder) {
if (holder instanceof FooterHolder) {
FooterHolder footerHolder = (FooterHolder) holder;
if (mNetworkState.isLoadFailed()) {
footerHolder.pb.setVisibility(View.GONE);
footerHolder.bt.setVisibility(View.VISIBLE);
footerHolder.tvMsg.setVisibility(View.VISIBLE);
footerHolder.tvMsg.setText(mErrorMsg);
} else {
footerHolder.pb.setVisibility(View.VISIBLE);
footerHolder.bt.setVisibility(View.GONE);
footerHolder.tvMsg.setVisibility(View.GONE);
}
return true;
}
return false;
}
@Override
public int getItemCount() {
return super.getItemCount() + (hasExtraRow() ? 1 : 0);
}
/** 根据网络请求状态结果控制展示 Footer UI */
public void setNetworkState(int state) {
Log.d(TAG, "loading state = " + state);
int preState = mNetworkState.getState();
boolean oldEx = hasExtraRow();
Log.d(TAG, "oldEx = " + oldEx);
mNetworkState.setState(state);
boolean newEx = hasExtraRow();
Log.d(TAG, "newEx = " + newEx);
if (!ObjectsCompat.equals(oldEx, newEx)) {
// 说明 Footer UI 需要切换,滑倒了最底部
if (oldEx) {
notifyItemRemoved(super.getItemCount());
} else {
notifyItemInserted(super.getItemCount());
}
} else if (newEx && !ObjectsCompat.equals(preState, state)) {
// 失败时
notifyItemChanged(getItemCount() - 1);
}
}
private boolean hasExtraRow() {
return !mNetworkState.isIdle() && !mNetworkState.isLoaded();
}
private static class FooterHolder extends RecyclerView.ViewHolder {
private ProgressBar pb;
private TextView tvMsg;
private Button bt;
private FooterHolder(@NonNull View itemView, @NonNull OnRetryListener listener) {
super(itemView);
pb = itemView.findViewById(R.id.item_wan_footer_pb);
tvMsg = itemView.findViewById(R.id.item_wan_footer_tv_msg);
bt = itemView.findViewById(R.id.item_wan_footer_bt_retry);
bt.setOnClickListener(new IntervalClickListener() {
@Override
protected void onWiseClick(View v) {
listener.onRetry();
}
});
}
}
/** 点击重试按钮监听 */
public interface OnRetryListener {
void onRetry();
}
private static class WanHolder extends RecyclerView.ViewHolder {
private TextView tvTitle;
private TextView tvUser;
private TextView tvClass;
private TextView tvTime;
private WanHolder(@NonNull View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.item_wan_tv_title);
tvUser = itemView.findViewById(R.id.item_wan_tv_user_name);
tvClass = itemView.findViewById(R.id.item_wan_tv_classification);
tvTime = itemView.findViewById(R.id.item_wan_tv_time);
}
}
}
继承PagedListAdapter
,内部提供了一个submitList(PagedList<T> pagedList)
用于改变数据源List
构造方法,需要提供一个DiffUtil
的比较差异WanDiffCallback
WanDiffCallback
public class WanDiffCallback extends DiffUtil.ItemCallback<WanItem.DataBean.DatasBean> {
@Override
public boolean areItemsTheSame(@NonNull WanItem.DataBean.DatasBean oldItem,
@NonNull WanItem.DataBean.DatasBean newItem) {
return oldItem.getTitle().endsWith(newItem.getTitle());
}
@Override
public boolean areContentsTheSame(@NonNull WanItem.DataBean.DatasBean oldItem,
@NonNull WanItem.DataBean.DatasBean newItem) {
return oldItem.getId() == newItem.getId();
}
}
依据item bean
自身固有属性,提供两个比较的不同的判定方式,告诉PagedListAdapter
,Item
数据有变化
setNetworkState()
方法,用来根据网络请求状态添加或者移除Footer
NetworkState
public class NetworkState {
private static final int IDLE = -1;
static final int RUNNING = 0;
static final int SUCCESS = 1;
static final int FAILED = 2;
private int mState = IDLE;
public static NetworkState create() {
return new NetworkState();
}
private NetworkState() {
}
public void setState(int state) {
if (mState != state) {
this.mState = state;
}
}
public int getState() {
return mState;
}
boolean isLoaded() {
return mState == SUCCESS;
}
boolean isIdle() {
return mState == IDLE;
}
boolean isLoading() {
return mState == RUNNING;
}
boolean isLoadFailed() {
return mState == FAILED;
}
}
2.2 WanVm
public class WanVm extends AndroidViewModel {
private MutableLiveData<Integer> mNetworkStateLiveData = new MutableLiveData<>();
private MutableLiveData<String> mErrorMsgLiveData = new MutableLiveData<>();
private MutableLiveData<Runnable> mRetryLiveData = new MutableLiveData<>();
public WanVm(@NonNull Application application) {
super(application);
}
void postNetworkState(int state) {
mNetworkStateLiveData.postValue(state);
}
void postErrorMsg(@NonNull String msg) {
mErrorMsgLiveData.postValue(msg);
}
void postRetry(@NonNull Runnable action) {
mRetryLiveData.postValue(action);
}
public MutableLiveData<Integer> networkStateLiveData() {
return mNetworkStateLiveData;
}
public MutableLiveData<String> errorMsgLiveData() {
return mErrorMsgLiveData;
}
public MutableLiveData<Runnable> retryLiveData() {
return mRetryLiveData;
}
@Override
protected void onCleared() {
super.onCleared();
}
}
mNetworkStateLiveData:
滑动过程中,用来发送标示网络请求状态
mErrorMsgLiveData:
网络请求错误的信息
mRetryLiveData:
重试的Runnable
任务
WanSourceFactory
提供了一个DataSource.Factory
用来创建DataSource
public class WanSourceFactory extends DataSource.Factory<Integer, WanItem.DataBean.DatasBean> {
private WanVm mWanVm;
public WanSourceFactory(WanVm wanVm) {
mWanVm = wanVm;
}
@Override
public DataSource<Integer, WanItem.DataBean.DatasBean> create() {
return new WanDataSource(mWanVm);
}
}
2.3 WanDataSource
public class WanDataSource extends PageKeyedDataSource<Integer, WanItem.DataBean.DatasBean> {
private static final String TAG = WanDataSource.class.getSimpleName();
private WanVm mWanVm;
private boolean mIsLoadInitial = true;
WanDataSource(WanVm wanVm) {
mWanVm = wanVm;
}
/** 初始化数据: 第 1 页 */
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params,
@NonNull LoadInitialCallback<Integer, WanItem.DataBean.DatasBean> callback) {
load(0, new LoadCallback() {
@Override
public void onSuccess(@NonNull List<WanItem.DataBean.DatasBean> list) {
callback.onResult(list, 0, 1);
}
@Override
public void onFailed(@NonNull String msg) {
Log.d(TAG, msg);
mWanVm.postRetry(() -> loadInitial(params, callback));
}
});
}
/** 上滑: 前一页 */
@Override
public void loadBefore(@NonNull LoadParams<Integer> params,
@NonNull PageKeyedDataSource.LoadCallback<Integer, WanItem.DataBean.DatasBean> callback) {
// 如果 loadInitial 从 0 开始,这个方法忽略
}
/** 下滑: 后一页 */
@Override
public void loadAfter(@NonNull LoadParams<Integer> params,
@NonNull PageKeyedDataSource.LoadCallback<Integer, WanItem.DataBean.DatasBean> callback) {
int page = params.key;
load(page, new LoadCallback() {
@Override
public void onSuccess(@NonNull List<WanItem.DataBean.DatasBean> list) {
callback.onResult(list, page + 1);
}
@Override
public void onFailed(@NonNull String msg) {
Log.d(TAG, msg);
mWanVm.postRetry(() -> loadAfter(params, callback));
}
});
}
private void load(@IntRange(from = 0) int page, @NonNull LoadCallback callback) {
if (!mIsLoadInitial) {
mWanVm.postNetworkState(NetworkState.RUNNING);
}
mIsLoadInitial = false;
OkHttpUtils.<WanItem>get()
.url(Urls.wan(page))
.tag(LOAD_TAG)
.build()
.execute(new OkWanItemCallback() {
@Override
public void onSuccess(OkResponse<WanItem> okResponse) {
WanItem wanItem = okResponse.body();
List<WanItem.DataBean.DatasBean> list = wanItem.getData().getDatas();
if (list == null || list.isEmpty()) {
onFailure(okResponse);
return;
}
mWanVm.postNetworkState(NetworkState.SUCCESS);
callback.onSuccess(list);
}
@Override
public void onFailure(OkResponse<WanItem> okResponse) {
super.onFailure(okResponse);
mWanVm.postNetworkState(NetworkState.FAILED);
mWanVm.postErrorMsg(okResponse.msg());
callback.onFailed(okResponse.msg());
}
});
}
private interface LoadCallback {
void onSuccess(@NonNull List<WanItem.DataBean.DatasBean> list);
void onFailed(@NonNull String msg);
}
}
loadInitial(),loadBefore(),loadAfter()
内的处理本身在Paging
内部回调在子线程
网络请求,自己看了OkHttpUtils
后,学着做了一个简单的封装,这里execute()
是同步的
在onFailure()
失败方法内,发送了网络请求状态,错误信息
由于loadInitial()
和loadAfter()
在失败后,Retry
重试时,需要不同的参数,就直接用Runnbale
给包装了下,执行时,直接根据网络请求库线程情况,使用Handler
或者Executor
都可以
除了网络请求的item bean
,其他代码基本都贴出来了
ItemKeyedDataSource,PositionalDataSource,PageKeyedDataSource
的使用场景差别,主要取决于数据源的提供结构,至于怎么分的页,怎么加载,何时决定加载的,目前不知道,需要再多了解
原理了,进阶使用之类的,可以看看Android 官方架构组件 Paging:分页库的设计美学
体验很好,滑动起来很顺畅,尤其网络好的情况下,但比较麻烦,之前封装的Adapter
需要单独再扩展适配下,而且加Header
和Footer
,不能直接加,需要做些处理
Paging
设计的很6,贼6,以后得多花点时间学习了解下
刚开始学用,有错误和不妥的地方,请指出
2019年马上结束了,写点东西,纪念下吧