android paging 库介绍

2023-09-25 19:14:38 浏览数 (1)

1.paging库简介

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

1.1 目录结构
代码语言:javascript复制
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方法

代码语言:javascript复制
    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 有如下变量

代码语言:javascript复制
    private final ArrayList<List<T>> mPages;

说明是按页存储数据。PagedList会从Datasource中加载数据,更准确的说是通过Datasource加载数据, 通过Config的配置,可以设置一次加载的数量以及预加载的数量。 除此之外,PagedList还可以向RecyclerView.Adapter发送更新的信号,驱动UI的刷新。 PagedList 有三个子类ContiguousPagedList,SnapshotPagedList,TiledPagedList。可以通过 create 方法找到

代码语言:javascript复制
    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库里提供了好些它的子类

代码语言:javascript复制
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 开始

代码语言:javascript复制
    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 方法

代码语言:javascript复制
    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

代码语言:javascript复制
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) {

        }
    }
}

用到的相关类贴出来

代码语言:javascript复制
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上。

代码语言:javascript复制
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 的实例。

代码语言:javascript复制
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
代码语言:javascript复制
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

0 人点赞