阅读 2736

Jetpack - Paging 简单使用(Java)

前两天想了解下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的方式,所以理所当然使用了PagingPageKeyedDataSource来获取数据

重点有两个东西WanDataSourceWanVm,但是需要提前知道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>
复制代码

网络错误时,UI

RecyclerViewitem布局不再贴出来


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自身固有属性,提供两个比较的不同的判定方式,告诉PagedListAdapterItem数据有变化


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需要单独再扩展适配下,而且加HeaderFooter,不能直接加,需要做些处理

Paging设计的很6,贼6,以后得多花点时间学习了解下

刚开始学用,有错误和不妥的地方,请指出

2019年马上结束了,写点东西,纪念下吧