jetpack之Paging

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++;
    }
}

两个关键方法loadInitialloadRange需要子类重写。

  • 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&lt;com.xinyartech.base.recyclerView.BaseRecyclerViewModel&gt;" />

        <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) 的原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值