Android 架构组件之 Paging

2024-02-25 16:58
文章标签 android 组件 架构 paging

本文主要是介绍Android 架构组件之 Paging,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 1. 为什么要使用 Paging Library?
    • 2. 分析 Paging 的组成及原理
      • 2.1 PagedList
      • 2.2 数据源 DataSource
      • 2.3 总结一下不同的数据源,如何创建 DataSource
      • 2.4 PagedListAdapter
    • 3. 通过一个简单的案例,介绍如何使用 Paging Library
    • 4. 最后对 Paging Library 进行简单的总结
    • 参考链接

Paging Library 是 Google 提出的分页加载库,本文将从以下几个方面对 Paging 进行介绍:

  • 为什么要使用 Paging Library?
  • 分析 Paging 的组成及原理
  • 通过一个简单的案例,介绍如何使用 Paging Library
  • 最后对 Paging Library 进行简单的总结

1. 为什么要使用 Paging Library?

我们经常需要处理大量数据,但大多数情况下,只需要加载和显示其中的一小部分。如果去请求用户不需要的数据,势必会浪费用户设备的电量和带宽。如果数据比较多情况下,消耗用户的流量也会比较多。

Paging Library 是 Google 提出的分页加载库,它可以妥善的逐步加载数据, 解决上面提到的痛点。此外:

  • Paging Library 可以与 RecyclerView 无缝结合;
  • Paging Library 还支持加载有限、或无限的 List,从而使得 RecyclerView 快速,无限滚动;
  • Paging Library 可以配合 LiveData、RxJava 集成使用,来观察界面中的数据变化;
  • Paging Library 可以选择本地数据库,网络或两者结合的方式作为分页数据的数据源,还可以自定义如何加载内容。

Paging Library 有这么多的特点,正是我们选择的使用它的主要原因。接下来分析一下它的组成及原理。

2. 分析 Paging 的组成及原理

Paging Library 的原理是,将数据分解成多个 List,使用 RecyclerView 中的 Adapter来观察 LiveDdata 中的数据变化,在此基础上加上分页功能,从而实现逐步加载内容。

我们来看一下具体的实现过程:

看过上面Paging Library 的实现过程,我们来总结一下:

  • DataSource 负责从数据源加载数据,它是连接 数据源与 PagedList 的桥梁,DataSource 的数据源可以是本地的数据库,也可以是网络,或者两者结合的方式;
  • PagedList 是List 的子类,从 DataSource 中取得的数据先放到 PagedList 中,我们可以在 PagedList 中配置每次加载多少条数据;
  • PagedListAdapter 是 RecyclerView.Adapter 的实现类,从 PagedList 过来的数据,经过 DiffUtil 计算出数据的差异,计算的过程是一个异步的过程;
  • 计算后的数据,通过 RecyclerView.Adapter 的 onBindViewHolder() 方法,更新到 UI 上。

看过了 Paging Library 具体的执行过程,我们来分析一下它的组成。我们先来看一下 Paging Library 相关的类图。

Paging Library 的核心组件是 PagedList 和 DataSource,在上面的类图中,用不同的颜色进行了区分。下面我们分别来介绍。

2.1 PagedList

PagedList 是一个集合类,它以分块的形式异步加载数据,每一块就称为一页。

在上面的类图中,我们可以看到:

  • PagedList 有四个内部类,分别是 Config、抽象类 Callback、抽象类 BoundaryCallback 和 Builder。
  • Config 类可以自定义 PagedList 从数据源加载数据的一些行为,比如每页加载多少条数据 pageSize,初始加载多少数据 mInitialLoadSizeHint,是否使用占位符 mEnablePlaceholders ,预加载距离 prefetchDistance 等。通常设置 mInitialLoadSizeHint 是pageSize 的整数倍,默认是3 倍。预加载距离 prefetchDistance ,即列表当距离加载边缘多远时触发分页的请求,通常应该是屏幕上可见项的数倍,默认与 pageSize 相等。
  • Callback 当数据被加载到 PagedList 中时会触发这个类中的回调方法。
  • BoundaryCallback 当 PagedList 到达可用数据的末端时(需要加载分页内容时)就会触发这个类中的回调方法;
  • Builder 是 PagedList 的生成器类,PagedList 的实例都是通过 Builder 类中的 build()方法产生。

在 PagedList 中,除了上面提到的这四个内部类的成员变量之外,还有两个比较重要的成员变量:

  • mMainThreadExecutor 将数据传递到 Adapter 的主线程;
  • mBackgroundThreadExecutor 加载数据的后台线程;

Paging Library 还提供了 LivePagedListBuilder类,用于获取 PagedList 中的 LiveData 对象,创建 LivePagedListBuilder 的参数,创建 DataSource.Factory 对象和分页配置对象。LivePagedListBuilder 获取 LiveData 对象的过程如下:

@AnyThread@NonNull@SuppressLint("RestrictedApi")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) {@Nullableprivate PagedList<Value> mList;@Nullableprivate DataSource<Key, Value> mDataSource;private final DataSource.InvalidatedCallback mCallback =new DataSource.InvalidatedCallback() {@Overridepublic void onInvalidated() {invalidate();}};@SuppressWarnings("unchecked") // for casting getLastKey to Key@Overrideprotected 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();}

可以看到,创建 PagedList 对象,还是通过 PagedList 的内部类 Builder 的 build()方法。

如果倾向于使用 RxJava,而不是 LiveData,可以使用 RxPagedListBuilder, 它的构建方式与LivePagedListBuilder类似,不同之处在于RxPagedListBuilder返回一个 Observable 对象或 Flowable 对象,而不是 LiveData 对象。

2.2 数据源 DataSource

再来看看 Paging Library 的另一个核心组成部分 DataSource。DataSource 是将数据加载到 PagedList 中的基类,任何数据都可以作为 DataSource 的来源,比如网络、数据库、文件等等。 DataSource.Factory 类可以用来创建 DataSource。

从上面的类图中,我们总结一下:

  • DataSource 是一个抽象的泛型类,接收两个泛型参数<Key,Value>,其中 Key 表示从数据源加载数据项的唯一标识,Value 与标识 Key 对应的数据项。DataSource 中定义了一个抽象的静态内部类 Factory<Key, Value>,是创建 DataSource 的工厂类。
  • DataSource 有两个直接的子类,分别是 PositionalDataSource 和 ContiguousDataSource。ContiguousDataSource 是一个非 public 的类,我们通常使用它的两个子类,PageKeyedDataSource 和 ItemKeyedDataSource 。
  • PositionalDataSource 和 ContiguousDataSource 这两个类最大的区别是对抽象方法 isContiguous() 的实现方式不同,PositionalDataSource 中的 isContiguous() 方法返回 false,ContiguousDataSource 中的 isContiguous() 方法返回 true。所以我们在代码中,能够使用的 DataSource 有三种,分别是 PositionalDataSource 和 ContiguousDataSource 的两个子类 PageKeyedDataSource 和 ItemKeyedDataSource 。
public abstract class DataSource<Key, Value> {/*** Returns true if the data source guaranteed to produce a contiguous set of items,* never producing gaps.*/abstract boolean isContiguous();
}abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> {@Overrideboolean isContiguous() {return true;}}public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {@Overrideboolean isContiguous() {return false;}}

关于数据源产生的数据项是否为连续的,结合后面三种 DataSource 的使用场景更好理解。

我们来看一下 PositionalDataSource、PageKeyedDataSource 和 ItemKeyedDataSource 分别适用哪些场景

  • 使用 PositionalDataSource,需要我们的实现类实现 loadInitial() 和 loadRange() 方法,适用于数据项总数固定,要通过特定的位置加载数据。比如从某个位置开始的 100 条数据;
  • 使用 PageKeyedDataSource,需要实现 loadInitial()、loadBefore() 和 loadAfter() 方法,适用于以页信息加载数据的场景。比如在网络加载数据的时候,需要通过 setNextKey() 和 setPreviousKey() 方法设置下一页和上一页的标识 Key。
  • 使用 ItemKeyedDataSource 除了需要实现 loadInitial()、loadBefore() 和 loadAfter() 方法以外,还要实现getKey() 方法,适用于所加载的数据依赖其他现有数据信息的场景。比如要加载的下一页的数据,依赖于当前页的数据。

2.3 总结一下不同的数据源,如何创建 DataSource

假设数据源是数据库,Room 存储库可以作为 Paging Library 的数据源,对于给定查询的关键字,Room 可以从 DAO 中返回 DataSource.Factory 对象,从而无缝处理 DataSource 的实现。

假设数据库是从网络加载的数据缓存,从 DAO中返回 DataSource.Factory 对象,还需要另外一个分页组件,BoundaryCallback,当界面显示缓存中靠近结尾的数据时,BoundaryCallback 将加载更多的数据,在获得更多的数据后,Paging Library 将自动更新界面,不要忘记将创建的 BoundaryCallback 对象与之前创建的 LivePagedListBuilder 对象进行关联,关联之后,PagedList 就可以使用它了。

仅将网络作为数据源,在这种情景中,需要创建 DataSource 和 DataSource.Factory 对象,选择 DataSource 类型时, 需要综合考虑后端 API 的架构,如果通过键值请求后端数据,使用 ItemKeyedDataSource。

举个例子,我们需要在某个特定日期起,github的前 100 项提交,该日期将成为 DataSource 的键,ItemKeyedDataSource 允许自定义如何加载初始页,以及如何加载某个键值前后的数据,如果后端数据返回的是分页后的,那么我们可以使用 PageKeyedDataSource,比如 Github API 中的 SearchRepository 就可以返回分页数据,我们在 Github API 的请求中,指定查询的关键字和要查询哪一页,同时也可以指定每个页面的项数,不管网络数据源的创建方式是什么,都需要创建 DataSource.Factory对象,有了 DataSource.Factory 对象就可以创建 DataSource。

2.4 PagedListAdapter

Paging Library 提供了 PagedListAdapter,可以将 PagedList 中的数据加载到 RecyclerView 中,PagedListAdapter 会在页加载时收到通知,收到新数据时,会使用 DiffUtil 精细计算更新。

在 PagedListAdapter 中使用的是 AsyncPagedListDiffer,从名字就能看出这是一个异步计算更新的过程。

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

在创建 PagedListAdapter 实例的时候,可以通过构造参数 DiffUtil.ItemCallback 对象,在 DiffUtil.ItemCallback 中可以来实现计算的规则。

public abstract static class ItemCallback<T> {public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);
}

3. 通过一个简单的案例,介绍如何使用 Paging Library

我们使用 Github 的 api,实现按照指定关键字检索仓库,按照 star 数量和仓库名称降序的方式,将检索到的结果显示到 UI 上。

1. 首先使用 PageList 来批量加载数据,比如将 List 替换为 PagedList:

data class RepoSearchResult(val data: LiveData<PagedList<Repo>>,val networkErrors: LiveData<String>
)

当创建PagedList时,它会立即加载第一块数据,并随着时间的推移随着内容的加载而扩展。PagedList 的大小是每次传递期间装载的数据项的数目。该类既支持无限列表,也支持元素数量固定的非常大的列表。

**2. 定义 DataSource,为 PagedList 准备加载的内容。**在我们的例子中,因为数据库是UI的主要来源,所以在 Dao 中可以把 DataSource.Factory 作为返回值类型,方便创建 DataSource 实例。

@Dao
interface RepoDao {fun reposByName(queryString: String): Factory<Int, Repo>
}

在 Repository 中通过返回的 DataSource.Factory来创建 DataSource 实例:

class GithubRepository(private val service: GithubService,private val cache: GithubLocalCache
) {/*** Search repositories whose names match the query.*/fun search(query: String): RepoSearchResult {Log.d("GithubRepository", "New query: $query")// Get data source factory from the local cacheval dataSourceFactory = cache.reposByName(query)// Construct the boundary callbackval boundaryCallback = RepoBoundaryCallback(query, service, cache)val networkErrors = boundaryCallback.networkErrorsval data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).setBoundaryCallback(boundaryCallback).build()return RepoSearchResult(data, networkErrors)}companion object {private const val DATABASE_PAGE_SIZE = 20}
}

**3.配置 PagedList,**这里使用 LivePagedListBuilder 来配置,配置的内容可以包括以下内容:

  • 由 PagedList 加载的页面的大小;
  • 加载的距离;
  • 第一次加载时要加载多少项;
  • 是否可以将空项添加到PagedList中,以表示尚未加载的数据。

4. 使用 PagedListAdapter、RecyclerView 将结果显示在 UI 上

class ReposAdapter :PagedListAdapter<Repo, androidx.recyclerview.widget.RecyclerView.ViewHolder>(REPO_COMPARATOR) {......companion object {private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean =oldItem.fullName == newItem.fullNameoverride fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean =oldItem == newItem}}
}

这里的 REPO_COMPARATOR 是 DiffUtil.ItemCallback 的实现类,确定了后台计算数据更新的规则。

**5. 处理RecyclerView 滚动,实现数据的网络更新。**通过 BoundaryCallback 来实现。

class RepoBoundaryCallback(private val query: String,private val service: GithubService,private val cache: GithubLocalCache
) : BoundaryCallback<Repo>() {override fun onZeroItemsLoaded() {requestAndSaveData(query)}override fun onItemAtEndLoaded(itemAtEnd: Repo) {requestAndSaveData(query)}private fun requestAndSaveData(query: String) {if (isRequestInProgress) returnisRequestInProgress = truesearchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE, { repos ->cache.insert(repos) {lastRequestedPage++isRequestInProgress = false}}, { error ->_networkErrors.postValue(error)isRequestInProgress = false})}
}

创建 BoundaryCallback 的实例,在创建 DataSource 实例时作为参数传入。

示例代码的运行效果:

完整的项目地址 示例代码地址

4. 最后对 Paging Library 进行简单的总结

先简单概括一下如何使用 Paging Library:

  • 首先,要定义 DataSource;
  • 需要的时候,创建 BoundaryCallback;
  • 使用LivePagedListBuilder创建 PagedList 的 LiveData;
  • 将 Adapter 转化为 PagedListAdapter,
  • 最后在 UI 中观察 PagedList 的 LiveData 对象,并将 更新后的PagedList 传给 PagedListAdapter。

我们再来看一下 Paging Library 的各个组成部分是如何系统工作的。

**首先,当 PagedList 创建时,**完成了两个工作:

当 PagedList 创建时,LiveData 会将 PagedList 传给 ViewModel。UI 监听到 PagedList 更新后,从 ViewModel 中取出 PagedList 传给 PagedListAdapter,最后更新在 UI 的 RecyclerView 上。这个过程如图中蓝色空心方块的运动过程。

当 PagedList 创建时,第二个工作是加载第一块数据,如果在 app 首次启动时,DataSource 中还没有数据,这时候会触发 BoundaryCallback.onZeroItemsLoaded() 方法,在我们的示例中,会从网络加载数据,并将这些数据持久化到数据库中。这个过程如图中橙色线的运动过程。

然后,当数据源中有数据后, PagedList 的新实例会被创建,这个实例最终通过 ViewModel 中的 LiveData 传到 PagedListAdapter,然后更新到 RecyclerView 上。这个过程如图中蓝色方块的运动过程。

**最后,当用户滑动屏幕触发加载下一页数据时,**如果数据源中还有可提供的数据时,重复上图中的过程。如果数据源中没有可以提供的数据,会触发 BoundaryCallback.onItemAtEndLoaded() 方法,BoundaryCallback 会从网络请求更多的数据,然后持久化到数据库中,然后根据新加载的数据,重新填充 UI。


至此,Android 架构组件 Paging 就介绍完了,下一篇我们来分析 Android 架构组件 Room 的使用。

更多内容,可以订阅 我的博客


参考链接

Android Paging
应用架构指南

这篇关于Android 架构组件之 Paging的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/746109

相关文章

JS常用组件收集

收集了一些平时遇到的前端比较优秀的组件,方便以后开发的时候查找!!! 函数工具: Lodash 页面固定: stickUp、jQuery.Pin 轮播: unslider、swiper 开关: switch 复选框: icheck 气泡: grumble 隐藏元素: Headroom

mybatis的整体架构

mybatis的整体架构分为三层: 1.基础支持层 该层包括:数据源模块、事务管理模块、缓存模块、Binding模块、反射模块、类型转换模块、日志模块、资源加载模块、解析器模块 2.核心处理层 该层包括:配置解析、参数映射、SQL解析、SQL执行、结果集映射、插件 3.接口层 该层包括:SqlSession 基础支持层 该层保护mybatis的基础模块,它们为核心处理层提供了良好的支撑。

百度/小米/滴滴/京东,中台架构比较

小米中台建设实践 01 小米的三大中台建设:业务+数据+技术 业务中台--从业务说起 在中台建设中,需要规范化的服务接口、一致整合化的数据、容器化的技术组件以及弹性的基础设施。并结合业务情况,判定是否真的需要中台。 小米参考了业界优秀的案例包括移动中台、数据中台、业务中台、技术中台等,再结合其业务发展历程及业务现状,整理了中台架构的核心方法论,一是企业如何共享服务,二是如何为业务提供便利。

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

系统架构设计师: 信息安全技术

简简单单 Online zuozuo: 简简单单 Online zuozuo 简简单单 Online zuozuo 简简单单 Online zuozuo 简简单单 Online zuozuo :本心、输入输出、结果 简简单单 Online zuozuo : 文章目录 系统架构设计师: 信息安全技术前言信息安全的基本要素:信息安全的范围:安全措施的目标:访问控制技术要素:访问控制包括:等保

Android 10.0 mtk平板camera2横屏预览旋转90度横屏拍照图片旋转90度功能实现

1.前言 在10.0的系统rom定制化开发中,在进行一些平板等默认横屏的设备开发的过程中,需要在进入camera2的 时候,默认预览图像也是需要横屏显示的,在上一篇已经实现了横屏预览功能,然后发现横屏预览后,拍照保存的图片 依然是竖屏的,所以说同样需要将图片也保存为横屏图标了,所以就需要看下mtk的camera2的相关横屏保存图片功能, 如何实现实现横屏保存图片功能 如图所示: 2.mtk