Android分页,Java代码官方Paging框架使用简析

转载请标明出处https://www.jianshu.com/p/b9c7a6e3e8d2

前言:最近有个需求,要考虑到系统资源以及网络请求的效率,需要做一个类似于现在市面上那种列表页面可以往上滑动不断加载item的效果,想着自己写逻辑,对控件recycleview的滑动到底部的事件进行判定之后请求数据,再去对adapter进行数据的判断增加然后视图刷新。一箩筐下来觉得好麻烦啊,就去Google了一下,发现官方提供了Paging库来处理这个场景。由于这个框架也是运用到LiveData,不熟悉的朋友可以先看一下之前LiveData的文章。

1.Paging

Paging是一个官方提供的分页库。使用这个库,我们只需要关心数据,分页和视图是不需要我们去关心的,这个库会帮我们实现。在这个库,最重要的就是关键组件是PagedList类。而且一般来说,这个库都会搭配着RXjava2或者LiveData来使用,友好的处理控件和数据的生命周期。本文的例子将会使用LiveData。

2.Paging的配合

2.1.数据

列表数据的来源,可分为本地数据和网络数据,本地数据官方是建议使用Room持久库来整理数据的,而网络数据,可以使用自己定义的数据源工厂,本文例子也将使用自定义数据源工厂。

22.界面

分页展示,这个库需要搭配recyclerview来进行展示,recyclerview也会有Paging提供的特殊adapter类。

3.使用

二话不多说,项目依赖走起。

   implementation "androidx.paging:paging-runtime:2.1.2"
   testImplementation "androidx.paging:paging-common:2.1.2"
   implementation "androidx.paging:paging-rxjava2:2.1.2"

分页列表的实现,界面由recyclerview来实现,这里recyclerview要注意,要继承Paging库提供的PagedListAdapter类,这是实现分页效果的关键。

public class AppleAdapter extends PagedListAdapter<apple, AppleAdapter.MyViewHolder> {

使用这个类我们不需要再重写getItemCount方法,PagedListAdapter自己重写了getItemCount,我们只需要通过设置DiffUtil来使得它可以对数据差异进行判断。可以通过item特有属性的对别或者item的整个对象的对比来得出差异,从而决定是否要更新到列表中去。

public AppleAdapter() {
        super(DIFF_CALLBACK);
    }

public static final DiffUtil.ItemCallback<apple> DIFF_CALLBACK = new DiffUtil.ItemCallback<apple>() {
        @Override
        public boolean areItemsTheSame(@NonNull apple oldApple, @NonNull apple newApple) {
            // User properties may have changed if reloaded from the DB, but ID is fixed
            return oldApple.getId() == newApple.getId();
        }

        @Override
        public boolean areContentsTheSame(@NonNull apple oldApple, @NonNull apple newApple) {
            // NOTE: if you use equals, your object must properly override Object#equals()
            // Incorrectly returning false here will result in too many animations.
            return newApple.equals(newApple);
        }
    };

其次就是数据了,这里我自己定义了数据源工厂类。配合了LiveData进行使用。先来看一下数据源工厂类。

 private class MyAppleSourceFactory extends DataSource.Factory<Integer, apple> {
        private MutableLiveData<MyAppleSource> sourceMutableLiveData = new MutableLiveData<>();
        private MyAppleSource source;

        @NonNull
        @Override
        public DataSource<Integer, apple> create() {
            source = new MyAppleSource();
            //查看Google的文档也没看明白这个liveData是为什么
            //但是猜测可能是想利用liveData对-生命周期进行监听,有懂的朋友可以评论不吝赐教。
            sourceMutableLiveData.postValue(source);
            return source;
        }
    }

    private class MyAppleSource extends ItemKeyedDataSource<Integer, apple> {

        @Override
        public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) {
            List<apple> items = getMoreMyApple(0);
            callback.onResult(items);
        }

        @Override
        public void loadAfter(@NonNull LoadParams params, @NonNull LoadCallback callback) {
            List<apple> items = getMoreMyApple((Integer) params.key);
            callback.onResult(items);
        }

        @Override
        public void loadBefore(@NonNull LoadParams params, @NonNull LoadCallback callback) {
        }

        @NonNull
        @Override
        public Integer getKey(@NonNull Apple item) {
            return item.getId();
        }
    }

工厂类内部实例化了一个Source类,这个类我这里是继承了ItemKeyedDataSource,他是规定整个分页是由item的某一个属性去获取数据的。官方还提供了其余2个Source类:PageKeyedDataSource,PositionalDataSource。解决各自的特定场景,需要的朋友可以自己Google了解一下。

然后看一下控件和工厂类数据的初始化,我这里是用了Viewmodel来实现demo的。

public LiveData<PagedList<Apple>> appleMutableLiveData;

public void initMyApple() {
        MyAppleSourceFactory appleSourceFactory = new MyAppleSourceFactory();
        myAppleSource = appleSourceFactory.create();
        //pageList的LiveData由activity这个UI层去进行监听。
        appleMutableLiveData = new LivePagedListBuilder(appleSourceFactory, 10).build();
    }

最后看一下Activity,

 appleMyBinding = DataBindingUtil.setContentView(this, R.layout.activity_apple_my);
        //苹果列表初始化,其实就是正常的recyclerview的管理器布局器设置
        initRVApple();
        myAppleViewModel = ViewModelProviders.of(this).get(AppleViewModel.class);
        //PagedList的LiveData初始化
        myAppleViewModel.initMyApple();
        myAppleViewModel.appleMutableLiveData.observe(this, appleVOS -> {
            //停止加载,通知回调
            myAppleViewModel.invalidateDataSource();
            //游戏列表数据变化监听,其实这个操作相当于向adapter传递数据的过程。
            //这也是一个LiveData连接PagedListAdapter的过程
            //PagedListAdapter会自动处理分页差异更新
            appleAdapter.submitList(appleVOS);
        });

这里关键其实是appleAdapter.submitList(appleVOS)。是由于这个方法,recyclerview和数据搭上了线。
到这里代码就写的差不多了。activity中recyclerview的初始化,数据源工厂类的自定义,viewmodel中的数据源工厂类初始化。通过这一系列操作就可以实现分页的效果。
在不知道他的实现原理的情况下,我们大胆猜测一下,整个流程是这样的,在ViewModel中LiveData通常是去通过model层获取数据设置数据到LiveData对象中的,也就是当分页开始请求数据的时候,数据来源于工厂类,它实际上会走Source类的loadInitial方法设置数据,之后界面每次数据不够显示了,就会调用loadAfter的方法去设置数据。实际中getMoreMyApple也是对应了Model层,是一个关于数据的网络请求方法,根据数据的id去请求数据。数据设置到LiveData中了,通过Adapter.submitList绑定到界面。那么关键就在于,装有PagedList的LiveData是如何得到的。咱们来看一下源码。

4.原理简析

官方写法是,在activity新建时viewmodel实例化,一起调用LivePagedListBuilder而获得一个LiveData对象,这个对象中包含了PagedList对象。这个LiveData是如何获得,咱们一层一层剥开看看。

appleMutableLiveData = new LivePagedListBuilder(appleSourceFactory, 10).build();

这个build到底做了什么可以得到一个LiveData,点开源码瞧瞧。

    public LiveData<PagedList<Value>> build() {
        return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
                ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
    }

他返回的是一个create方法执行得到的结果。看看create方法。

    private static <Key, Value> LiveData<PagedList<Value>> create(
            @Nullable final Key initialLoadKey,
            @NonNull final PagedList.Config config,
            @Nullable final PagedList.BoundaryCallback boundaryCallback,
            @NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
            @NonNull final Executor notifyExecutor,
            @NonNull final Executor fetchExecutor) {
        return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
            @Nullable
            private PagedList<Value> mList;
            @Nullable
            private DataSource<Key, Value> mDataSource;

            private final DataSource.InvalidatedCallback mCallback =
                    new DataSource.InvalidatedCallback() {
                        @Override
                        public void onInvalidated() {
                            invalidate();
                        }
                    };

            @SuppressWarnings("unchecked") // for casting getLastKey to Key
            @Override
            protected PagedList<Value> compute() {
                @Nullable Key initializeKey = initialLoadKey;
                if (mList != null) {
                    initializeKey = (Key) mList.getLastKey();
                }

                do {
                    if (mDataSource != null) {
                        mDataSource.removeInvalidatedCallback(mCallback);
                    }

                    mDataSource = dataSourceFactory.create();
                    mDataSource.addInvalidatedCallback(mCallback);

                    mList = new PagedList.Builder<>(mDataSource, config)
                            .setNotifyExecutor(notifyExecutor)
                            .setFetchExecutor(fetchExecutor)
                            .setBoundaryCallback(boundaryCallback)
                            .setInitialKey(initializeKey)
                            .build();
                } while (mList.isDetached());
                return mList;
            }
        }.getLiveData();
    }

create方法返回的则是一个ComputableLiveData类的实例化对象的getLiveData的值,看看这个ComputableLiveData类构造方法以及这个getLiveData方法。

 public ComputableLiveData(@NonNull Executor executor) {
        mExecutor = executor;
        mLiveData = new LiveData<T>() {
            @Override
            protected void onActive() {
                mExecutor.execute(mRefreshRunnable);
            }
        };
    }

   @NonNull
    public LiveData<T> getLiveData() {
        return mLiveData;
    }

原来这一系列下来是一个LiveData对象实例化以及返回的过程。而且当这个liveData启用的时候会在线程池中运行一个子线程。咱们看看这个子线程了干了些什么。

    final Runnable mRefreshRunnable = new Runnable() {
        @WorkerThread
        @Override
        public void run() {
            boolean computed;
            do {
                computed = false;
                // compute can happen only in 1 thread but no reason to lock others.
                if (mComputing.compareAndSet(false, true)) {
                    // as long as it is invalid, keep computing.
                    try {
                        T value = null;
                        while (mInvalid.compareAndSet(true, false)) {
                            computed = true;
                            value = compute();
                        }
                        if (computed) {
                            mLiveData.postValue(value);
                        }
                    } finally {
                        // release compute lock
                        mComputing.set(false);
                    }
                }
                // check invalid after releasing compute lock to avoid the following scenario.
                // Thread A runs compute()
                // Thread A checks invalid, it is false
                // Main thread sets invalid to true
                // Thread B runs, fails to acquire compute lock and skips
                // Thread A releases compute lock
                // We've left invalid in set state. The check below recovers.
            } while (computed && mInvalid.get());
        }
    };

这个子线程中对LiveData进行了postValue,这下子就清楚,这就是build方法返回的装有PagedList的liveData数据变化,UI也会变化了。而这个postValue的值来源于compute抽象方法的,咱们回头看看在LivePagedListBuilder中实例化的ComputableLiveData对象的compute方法具体是怎么实现的。

           @Override
            protected PagedList<Value> compute() {
                @Nullable Key initializeKey = initialLoadKey;
                if (mList != null) {
                    initializeKey = (Key) mList.getLastKey();
                }

                do {
                    if (mDataSource != null) {
                        mDataSource.removeInvalidatedCallback(mCallback);
                    }

                    mDataSource = dataSourceFactory.create();
                    mDataSource.addInvalidatedCallback(mCallback);

                    mList = new PagedList.Builder<>(mDataSource, config)
                            .setNotifyExecutor(notifyExecutor)
                            .setFetchExecutor(fetchExecutor)
                            .setBoundaryCallback(boundaryCallback)
                            .setInitialKey(initializeKey)
                            .build();
                } while (mList.isDetached());
                return mList;
            }

他post的value原来是来自于这个方法新建的PagedList对象,而他的值就是来源于dataSourceFactory的mDataSource对象的。这个factory对象就是我们build的时候传入的自定义数据源工厂类了。然后再来看看这个PagedList是怎么build生成一个PagedList对象的。

   public PagedList<Value> build() {
            // TODO: define defaults, once they can be used in module without android dependency
            if (mNotifyExecutor == null) {
                throw new IllegalArgumentException("MainThreadExecutor required");
            }
            if (mFetchExecutor == null) {
                throw new IllegalArgumentException("BackgroundThreadExecutor required");
            }

            //noinspection unchecked
            return PagedList.create(
                    mDataSource,
                    mNotifyExecutor,
                    mFetchExecutor,
                    mBoundaryCallback,
                    mConfig,
                    mInitialKey);
        }
    }

由PagedList类create方法实例化的,往下看。

static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
            @NonNull Executor notifyExecutor,
            @NonNull Executor fetchExecutor,
            @Nullable BoundaryCallback<T> boundaryCallback,
            @NonNull Config config,
            @Nullable K key) {
        if (dataSource.isContiguous() || !config.enablePlaceholders) {
            int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
            if (!dataSource.isContiguous()) {
                //noinspection unchecked
                dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
                        .wrapAsContiguousWithoutPlaceholders();
                if (key != null) {
                    lastLoad = (Integer) key;
                }
            }
            ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<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);
        }
    }

看着源码,这个source对象通过一系列判断最后会转为两种PagedList类,不过ContiguousDataSource比较特别,我们点进去看看。

ContiguousDataSource<Integer, T> wrapAsContiguousWithoutPlaceholders() {
        return new ContiguousWithoutPlaceholdersWrapper<>(this);
    }

ContiguousWithoutPlaceholdersWrapper(
                @NonNull PositionalDataSource<Value> source) {
            mSource = source;
        }
ContiguousPagedList(
            @NonNull ContiguousDataSource<K, V> dataSource,
            @NonNull Executor mainThreadExecutor,
            @NonNull Executor backgroundThreadExecutor,
            @Nullable BoundaryCallback<V> boundaryCallback,
            @NonNull Config config,
            final @Nullable K key,
            int lastLoad) {
        super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
                boundaryCallback, config);
        mDataSource = dataSource;
        mLastLoad = lastLoad;

        if (mDataSource.isInvalid()) {
            detach();
        } else {
            mDataSource.dispatchLoadInitial(key,
                    mConfig.initialLoadSizeHint,
                    mConfig.pageSize,
                    mConfig.enablePlaceholders,
                    mMainThreadExecutor,
                    mReceiver);
        }
        mShouldTrim = mDataSource.supportsPageDropping()
                && mConfig.maxSize != Config.MAX_SIZE_UNBOUNDED;
    }

TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
            @NonNull Executor mainThreadExecutor,
            @NonNull Executor backgroundThreadExecutor,
            @Nullable BoundaryCallback<T> boundaryCallback,
            @NonNull Config config,
            int position) {
        super(new PagedStorage<T>(), mainThreadExecutor, backgroundThreadExecutor,
                boundaryCallback, config);
        mDataSource = dataSource;

        final int pageSize = mConfig.pageSize;
        mLastLoad = position;

        if (mDataSource.isInvalid()) {
            detach();
        } else {
            final int firstLoadSize =
                    (Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize;

            final int idealStart = position - firstLoadSize / 2;
            final int roundedPageStart = Math.max(0, idealStart / pageSize * pageSize);

            mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
                    pageSize, mMainThreadExecutor, mReceiver);
        }
    }

这两个PagedList方法逻辑相似一上来就会对DataSource再判断,然后决定是否走PositionalDataSource的dispatchLoadInitial方法。
这个方法看着好眼熟,会让人想起自定义的source类不是吗。咱们往下看。

 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);

        // If initialLoad's callback is not called within the body, we force any following calls
        // to post to the UI thread. This constructor may be run on a background thread, but
        // after constructor, mutation must happen on UI thread.
        callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
    }

这个方法,直接就调用我们自定义source类的loadInitial方法了。就可以拿到自定义请求方法得到的PagedList了,还有loadAfter和loadBefore的调用过程这里就不再深入了。

接下来来看一下,这个source中的关于数据加载时机的回调,是怎么配合PagedListAdapter的。看看Adapter。

 public AppleAdapter() {
        super(DIFF_CALLBACK);
    }

protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
        mDiffer.addPagedListListener(mListener);
    }

 private final AsyncPagedListDiffer.PagedListListener<T> mListener =
            new AsyncPagedListDiffer.PagedListListener<T>() {
        @Override
        public void onCurrentListChanged(
                @Nullable PagedList<T> previousList, @Nullable PagedList<T> currentList) {
            PagedListAdapter.this.onCurrentListChanged(currentList);
            PagedListAdapter.this.onCurrentListChanged(previousList, currentList);
        }
    };

这里先记住,它内部会实例化一个AsyncPagedListDiffer对象并且赋予回调以及设置PagedListListener。
然后我们来看一下PagedListAdapter的submit方法。因为是依赖这个方法实现数据和界面的绑定的。

  appleAdapter.submitList(apples);
  public void submitList(@Nullable PagedList<T> pagedList) {
        mDiffer.submitList(pagedList);
    }

发现是调用了AsyncPagedListDiffer这个类的submitList方法。往里面深处翻发现

public void submitList(@Nullable final PagedList<T> pagedList,
            @Nullable final Runnable commitCallback) {

这里有个关键方法,onCurrentListChanged(previous, null, commitCallback);

private void onCurrentListChanged(
            @Nullable PagedList<T> previousList,
            @Nullable PagedList<T> currentList,
            @Nullable Runnable commitCallback) {
        for (PagedListListener<T> listener : mListeners) {
            listener.onCurrentListChanged(previousList, currentList);
        }
        if (commitCallback != null) {
            commitCallback.run();
        }
    }

实际上他就是调用了上面PagedListListener的onCurrentListChanged,走的是PagedListAdapter的onCurrentListChanged方法的。看了一下源码注释和翻了一下文档,这是一个当前PagedList更新时调用的两个方法。

到这里就差不多知道整个流程了。通过LivePagedListBuilder拿到工厂类的Source,通过一系列回调走到我们自定义loadInitial方法,由此拿到PageList对象,将一个PagedList对象传递给Adapter,当页面变化,调用AsyncPagedListDiffer的onCurrentListChanged,就会触发Adapter的onCurrentListChanged。

5.总结

刚开始用的时候,由于都不知道其原理,只能先一边看文档一边写个demo,功能实现了,就是搞不明白为什么要写factory为什么要写source,为什么是LivePagedListBuilder去build拿到LiveData的,为什么通过submitList就可以让数据绑定到控件,然后随着控件滑动就可以触发source的loadAfter了。通过翻看源码结合注释以及翻看文档。终于懂得一二,总算知道了为什么可以通过这样一个流程实现一个分页的效果了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容