android paging 库介绍

作者: 勇敢地追 | 来源:发表于2021-06-24 09:52 被阅读0次

    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

    相关文章

      网友评论

        本文标题:android paging 库介绍

        本文链接:https://www.haomeiwen.com/subject/insyyltx.html