android paging 库介绍

1.paging库简介

Paging 使您的应用程序配合RecyclerView更容易从数据源中高效优雅地加载所需的数据,不会因为数据库数据量大而造成查询时间过长。说白了就是分页加载的优化。

1.1 目录结构
implementation "androidx.paging:paging-runtime:2.1.2"

之所以没用最新的是因为kotlin版本号冲突,所以降低了版本


paging.png
1.2 重要的类介绍

paging库最重要的三个类就是DataSource,PageList,PageListAdapter。

(1)PageListAdapter

PagedListAdapter是通过RecyclerView.Adapter实现,用于展示PagedList的数据。它本身并没有比adapter多多少东西。主要需要注意 AsyncPagedListDiffer 这个辅助类。它负责监听PagedList的更新, Item数量的统计等功能。当数据源变动产生新的PagedList,PagedAdapter会在后台线程中比较前后两个PagedList的差异,然后调用notifyItem…()方法更新RecyclerView。具体来看看PagedListAdapter的submitList方法

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

        ......

        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);//*******************
                        }
                    }
                });
            }
        });
    }

    void latchPagedList(
            @NonNull PagedList<T> newList,
            @NonNull PagedList<T> diffSnapshot,
            @NonNull DiffUtil.DiffResult diffResult,
            int lastAccessIndex,
            @Nullable Runnable commitCallback) {

        PagedList<T> previousSnapshot = mSnapshot;
        mPagedList = newList;
        mSnapshot = null;

        // dispatch update callback after updating mPagedList/mSnapshot
        PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
                previousSnapshot.mStorage, newList.mStorage, diffResult);
        ......
    }

最后一行 PagedStorageDiffHelper.dispatchDiff 传进去的第一个参数 mUpdateCallback内部就实现了 mAdapter 的 notifyItem 等方法。具体是 ListUpdateCallback
简单来说就是 调用了submitList 就没必要再去调用 notify 方法了

(2) PagedList

PageList继承AbstractList,支持所有List的操作。同时它还有一个重要的成员变量,PagedStorage。PagedStorage 有如下变量

    private final ArrayList<List<T>> mPages;

说明是按页存储数据。PagedList会从Datasource中加载数据,更准确的说是通过Datasource加载数据, 通过Config的配置,可以设置一次加载的数量以及预加载的数量。 除此之外,PagedList还可以向RecyclerView.Adapter发送更新的信号,驱动UI的刷新。
PagedList 有三个子类ContiguousPagedList,SnapshotPagedList,TiledPagedList。可以通过 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) {
            ......
            ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
            return new ContiguousPagedList<>(***);
        } else {
            return new TiledPagedList<>((PositionalDataSource<T>) dataSource,);
        }
    }

SnapshotPagedList 这个暂时可以不管,类似于弄了一个副本。ContiguousPagedList和TiledPagedList之后再介绍

(3)DataSource

DataSource<Key, Value>从字面意思理解是一个数据源,其中key对应加载数据的条件信息,Value对应加载数据的实体类。
DataSource是一个抽象类,但是我们不能直接继承它实现它的子类。但是Paging库里提供了好些它的子类

DataSource --- ContiguousDataSource --- ItemKeyedDataSource --- WrapperItemKeyedDataSource
DataSource --- ContiguousDataSource --- PageKeyedDataSource --- WrapperPageKeyedDataSource
DataSource --- PositionalDataSource --- ListDataSource
DataSource --- PositionalDataSource --- WrapperPositionalDataSource
  • PageKeyedDataSource<Key, Value>:适用于目标数据根据页信息请求数据的场景,即Key 字段是页相关的信息。比如请求的数据的参数中包含类似next/previous页数的信息。
  • ItemKeyedDataSource<Key, Value>:适用于目标数据的加载依赖特定item的信息, 即Key字段包含的是Item中的信息,比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的ID时,该场景多出现于论坛类应用评论信息的请求。
  • PositionalDataSource<T>:适用于目标数据总数固定,通过特定的位置加载数据,这里Key是Integer类型的位置信息,T即Value。 比如从数据库中的1200条开始加在20条数据。

还有其他的,比如 ListDataSource ,其实就是已经定制好的,可以直接用的

(4) PageKeyedDataSource 和 ContiguousPagedList

一般的网络请求都是分页的,所以我把这个单独拿出来分析一下。
我们从 ContiguousPagedList 的 loadAroundInternal 开始

    protected void loadAroundInternal(int index) {
        int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index,
                mStorage.getLeadingNullCount());
        int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index,
                mStorage.getLeadingNullCount() + mStorage.getStorageCount());

        mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
        if (mPrependItemsRequested > 0) {
            schedulePrepend();
        }

        mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
        if (mAppendItemsRequested > 0) {
            scheduleAppend();
        }
    }

    private void scheduleAppend() {
        if (mAppendWorkerState != READY_TO_FETCH) {
            return;
        }
        mAppendWorkerState = FETCHING;

        final int position = mStorage.getLeadingNullCount()
                + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();

        // safe to access first item here - mStorage can't be empty if we're appending
        final V item = mStorage.getLastLoadedItem();
        mBackgroundThreadExecutor.execute(new Runnable() {
            @Override
            public void run() {
                if (isDetached()) {
                    return;
                }
                if (mDataSource.isInvalid()) {
                    detach();
                } else {
                    mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
                            mMainThreadExecutor, mReceiver);
                }
            }
        });
    }

走 mDataSource.dispatchLoadAfter 方法

    final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
            int pageSize, @NonNull Executor mainThreadExecutor,
            @NonNull PageResult.Receiver<Value> receiver) {
        @Nullable Key key = getNextKey();
        if (key != null) {
            loadAfter(new LoadParams<>(key, pageSize),
                    new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
        } else {
            receiver.onPageResult(PageResult.APPEND, PageResult.<Value>getEmptyResult());
        }
    }

如果设置了key,就自己实现loadAfter。如果没设置,就用 ContiguousPagedList 默认的 mReceiver。在里面可以看到 mStorage.appendPage

2.自己动手实现一个 paging demo

首先我们来简单看一下Paging库的工作示意图,主要是分为如下几个步骤

  • 使用DataSource从服务器获取或者从本地数据库获取数据(需要自己实现)
  • 将数据保存到PageList中(会根据DataSource类型来生成对应的PageList,paging库已实现)
  • 将PageList的数据submitList给PageListAdapter(需要自己调用)
  • PageListAdapter在后台线程对比原来的PageList和新的PageList,生成新PageList(Paging库已实现对比操作,用户只需提供DiffUtil.ItemCallback实现)
    PageListAdapter通知RecyclerView更新
(1)使用DataSource从服务器获取数据

这里我们就用官方demo的url做测试。因为是分页加载的,所以肯定选用PageKeyedDataSource

public class UserPageKeyedDataSource extends PageKeyedDataSource<String, Repo> {

    private int pageNum = 0;
    private GithubService service;

    UserPageKeyedDataSource(GithubService service) {
        this.service = service;
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<String> params, @NonNull LoadInitialCallback<String, Repo> callback) {
        pageNum = 0;
        try {
            Response<RepoSearchResponse> response = service.searchRepos("Android" + IN_QUALIFIER, pageNum, 20).execute();
            callback.onResult(response.body().getItems(), "", "");
        } catch (Exception e) {

        }
    }

    /**
     * 请求上一页数据(基本不用)
     */
    @Override
    public void loadBefore(@NonNull LoadParams<String> params, @NonNull LoadCallback<String, Repo> callback) {

    }

    /**
     * 请求下一页数据
     */
    @Override
    public void loadAfter(@NonNull LoadParams<String> params, @NonNull LoadCallback<String, Repo> callback) {
        pageNum++;
        try {
            Response<RepoSearchResponse> response = service.searchRepos("Android" + IN_QUALIFIER, pageNum, 20).execute();
            callback.onResult(response.body().getItems(), "");
        } catch (Exception e) {

        }
    }
}

用到的相关类贴出来

const val IN_QUALIFIER = "in:name,description"

/**
 * Github API communication setup via Retrofit.
 */
interface GithubService {
    /**
     * Get repos ordered by stars.
     */
    @GET("search/repositories?sort=stars")
    fun searchRepos(
        @Query("q") query: String,
        @Query("page") page: Int,
        @Query("per_page") itemsPerPage: Int
    ): Call<RepoSearchResponse>

    companion object {
        private const val BASE_URL = "https://api.github.com/"

        fun create(): GithubService {
            val logger = HttpLoggingInterceptor()
            logger.level = Level.BASIC

            val client = OkHttpClient.Builder()
                .addInterceptor(logger)
                .build()
            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                //.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(IoScheduler()))
                .build()
                .create(GithubService::class.java)
        }
    }
}

data class RepoSearchResponse(
    @SerializedName("total_count") val total: Int = 0,
    @SerializedName("items") val items: List<Repo> = emptyList(),
    val nextPage: Int? = null
)

data class Repo(
    @field:SerializedName("id") val id: Long,
    @field:SerializedName("name") val name: String,
    @field:SerializedName("full_name") val fullName: String,
    @field:SerializedName("description") val description: String?,
    @field:SerializedName("html_url") val url: String,
    @field:SerializedName("stargazers_count") val stars: Int,
    @field:SerializedName("forks_count") val forks: Int,
    @field:SerializedName("language") val language: String?
)
(2)配置PageList

PageList主要负责控制 第一次默认加载多少数据,之后每一次加载多少数据,如何加载 等等。同时将数据的变更反映到UI上。

val adapter = MyPagedAdapter()
        recycleview.layoutManager = LinearLayoutManager(this)
        recycleview.adapter = adapter

        val sourceFactory = UserDataSourceFactory(GithubService.create())

        LivePagedListBuilder(sourceFactory, 20)
            //.setFetchExecutor()
            .build().observe(this,
                Observer<PagedList<Repo>> {
                    adapter.submitList(it)
                })

这里我们用LiveData来观察。注意她有个参数是 DataSource.Factory。这是DataSource 的内部工厂类,通过create()方法就可以获得DataSource 的实例。

public class UserDataSourceFactory extends DataSource.Factory<String, Repo> {

    private GithubService service;

    UserDataSourceFactory(GithubService service) {
        this.service = service;
    }

    @NonNull
    @Override
    public DataSource<String, Repo> create() {
        return new UserPageKeyedDataSource(service);
    }
}
(3)配置adapter
public class MyPagedAdapter extends PagedListAdapter<Repo, MyPagedAdapter.UserHolder> {

    public MyPagedAdapter() {
        super(callback);
    }

    @NonNull
    @Override
    public MyPagedAdapter.UserHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new UserHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout, parent, false));
    }

    @Override
    public void onBindViewHolder(@NonNull MyPagedAdapter.UserHolder holder, int position) {
        holder.bind(getItem(position));
    }

    static class UserHolder extends RecyclerView.ViewHolder {

        TextView mText;

        public UserHolder(@NonNull View itemView) {
            super(itemView);
            mText = itemView.findViewById(R.id.txt);
        }

        public void bind(Repo repo) {
            mText.setText(repo.getFullName());
        }
    }

    /**
     * DiffCallback的接口实现中定义比较的规则,比较的工作则是由PagedStorageDiffHelper来完成
     */
    private static final DiffUtil.ItemCallback<Repo> callback = new DiffUtil.ItemCallback<Repo>() {
        @Override
        public boolean areItemsTheSame(@NonNull Repo oldItem, @NonNull Repo newItem) {
            return oldItem.getId() == newItem.getId();
        }

        @Override
        public boolean areContentsTheSame(@NonNull Repo oldItem, @NonNull Repo newItem) {
            return oldItem.getFullName().equals(newItem.getFullName());
        }
    };
}

3.参考

Android Paging library详解(一)
Android Paging library详解(二)
Android Paging

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

推荐阅读更多精彩内容