Paging 库能作为jetpack的一部分,干嘛用的
通过Paging库,您可以更轻松地在应用程序的用户界面内逐步优雅地加载数据。
本篇文章demo实例来源于Google的Paging的CodeLable:
Demo样式如下:
包含以下功能:
- 支持分页
- 添加laoding,header,footer
- 添加分割线
- 支持从网咯和本地获取数据
源码下载:
git clone https://github.com/googlecodelabs/android-paging
PagingSource
PagingSource实现定义了数据源以及如何从该源中检索数据。 当用户滚动RecyclerView时,PagingData从PagingSource中获取数据
abstract class PagingSource<Key : Any, Value : Any> {}是个抽象类,其中包含了两个泛型
- **Key: **用来加载哪些数据的关键词的数据类型,比如通过
Int类型的pageNumber来确定拉取哪一页的数据,或者String类型的token等。 - **Value: **通过PageSource加载到的数据的类型。通常这个类型会传递给
PagingDataAdapter用来显示RecyclerView上的数据。
下面自定义一个继承PagingSorce的GithubPagingSource:
/**
* @author: frc
* @description:
* @date: 2020/10/5 5:59 AM
*
*/
class GithubPagingSource : PagingSource<Int,Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
TODO("Not yet implemented")
}
}
Key是Int类型,Value对应的类是Repo。Repo是Demo中recyclerView的数据类型:
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?
)
自定义的GithubPagingSource需要实现load(params: LoadParams)方法。看方法命名就猜到是让开发者自己实现数据加载逻辑的。前面加了suspend说明是异步的。下面具体说这个方法。
LoadParams
load()函数中的参数是LoadParams,它是由架构返回的数据类型,其中包含了跟请求相关的设置,这些设置一般在PagingConfig中都设置好了。下面看下LoadParams的源码:
path: xxx/paging-common-3.0.0-alpha07-sources.jar!/androidx/paging/PagingSource.kt
/**
* Params for a load request on a [PagingSource] from [PagingSource.load].
*/
sealed class LoadParams<Key : Any> constructor(
/**
* Requested number of items to load.
*
* Note: It is valid for [PagingSource.load] to return a [LoadResult] that has a different
* number of items than the requested load size.
*/
val loadSize: Int,
/**
* From [PagingConfig.enablePlaceholders], true if placeholders are enabled and the load
* request for this [LoadParams] should populate [LoadResult.Page.itemsBefore] and
* [LoadResult.Page.itemsAfter] if possible.
*/
val placeholdersEnabled: Boolean
) {
/**
* Key for the page to be loaded.
*
* [key] can be `null` only if this [LoadParams] is [Refresh], and either no `initialKey`
* is provided to the [Pager] or [PagingSource.getRefreshKey] from the previous
* [PagingSource] returns `null`.
*
* The value of [key] is dependent on the type of [LoadParams]:
* * [Refresh]
* * On initial load, the nullable `initialKey` passed to the [Pager].
* * On subsequent loads due to invalidation or refresh, the result of
* [PagingSource.getRefreshKey].
* * [Prepend] - [LoadResult.Page.prevKey] of the loaded page at the front of the list.
* * [Append] - [LoadResult.Page.nextKey] of the loaded page at the end of the list.
*/
abstract val key: Key?
...
internal companion object {
fun <Key : Any> create(
loadType: LoadType,
key: Key?,
loadSize: Int,
placeholdersEnabled: Boolean,
pageSize: Int
): LoadParams<Key> = when (loadType) {
LoadType.REFRESH -> Refresh(
key = key,
loadSize = loadSize,
placeholdersEnabled = placeholdersEnabled,
pageSize = pageSize
)
LoadType.PREPEND -> Prepend(
loadSize = loadSize,
key = requireNotNull(key) {
"key cannot be null for prepend"
},
placeholdersEnabled = placeholdersEnabled,
pageSize = pageSize
)
LoadType.APPEND -> Append(
loadSize = loadSize,
key = requireNotNull(key) {
"key cannot be null for append"
},
placeholdersEnabled = placeholdersEnabled,
pageSize = pageSize
)
}
}
}
LoadParams的构造函数有2个参数(pageSize已经过期):loadSize和placeholdersEnabled。
- loadSize:表示当前加载多少条数据。
- placeholdersEnabled:如果PageSource提供了null的placeholders,PageData是否显示。
它内部还有个属性:Key
前面提到PageSource有两个泛型,其中一个就是Key。它代表当前请求的关键词,这里的Key也是相同意思,不过它表示的是上次的请求的关键词。比如说:如果我们是根据一个Int类型的pagePosition作为分页的关键词,第一次可以是0,那么滑到底部触发load函数时候我们可以拿到Key=0,再进行请求时我们就可以把pagePosition设置为1。
所以demo中下面代码就能理解了:
private const val GITHUB_STARTING_PAGE_INDEX = 1
class GithubPagingSource(private val service: GithubService,
private val query: String) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
val apiQuery = query + IN_QUALIFIER
val response = service.searchRepos(apiQuery, position, params.loadSize)
...
}
}
LoadResult
LoadResult是load函数的返回对象的数据结构,是对返回对象的统一封装。
path: xxx/paging-common-3.0.0-alpha07-sources.jar!/androidx/paging/PagingSource.kt
/**
* Result of a load request from [PagingSource.load].
*/
sealed class LoadResult<Key : Any, Value : Any> {
/**
* Error result object for [PagingSource.load].
*
* This return type indicates an expected, recoverable error (such as a network load
* failure). This failure will be forwarded to the UI as a [LoadState.Error], and may be
* retried.
*
* @sample androidx.paging.samples.pageKeyedPagingSourceSample
*/
data class Error<Key : Any, Value : Any>(
val throwable: Throwable
) : LoadResult<Key, Value>()
/**
* Success result object for [PagingSource.load].
*
* @sample androidx.paging.samples.pageKeyedPage
* @sample androidx.paging.samples.pageIndexedPage
*/
data class Page<Key : Any, Value : Any> constructor(
/**
* Loaded data
*/
val data: List<Value>,
/**
* [Key] for previous page if more data can be loaded in that direction, `null`
* otherwise.
*/
val prevKey: Key?,
/**
* [Key] for next page if more data can be loaded in that direction, `null` otherwise.
*/
val nextKey: Key?,
/**
* Optional count of items before the loaded data.
*/
@IntRange(from = COUNT_UNDEFINED.toLong())
val itemsBefore: Int = COUNT_UNDEFINED,
/**
* Optional count of items after the loaded data.
*/
@IntRange(from = COUNT_UNDEFINED.toLong())
val itemsAfter: Int = COUNT_UNDEFINED
) : LoadResult<Key, Value>() {
/**
* Success result object for [PagingSource.load].
*
* @param data Loaded data
* @param prevKey [Key] for previous page if more data can be loaded in that direction,
* `null` otherwise.
* @param nextKey [Key] for next page if more data can be loaded in that direction,
* `null` otherwise.
*/
constructor(
data: List<Value>,
prevKey: Key?,
nextKey: Key?
) : this(data, prevKey, nextKey, COUNT_UNDEFINED, COUNT_UNDEFINED)
init {
require(itemsBefore == COUNT_UNDEFINED || itemsBefore >= 0) {
"itemsBefore cannot be negative"
}
require(itemsAfter == COUNT_UNDEFINED || itemsAfter >= 0) {
"itemsAfter cannot be negative"
}
}
companion object {
const val COUNT_UNDEFINED = Int.MIN_VALUE
@Suppress("MemberVisibilityCanBePrivate") // Prevent synthetic accessor generation.
internal val EMPTY = Page(emptyList(), null, null, 0, 0)
@Suppress("UNCHECKED_CAST") // Can safely ignore, since the list is empty.
internal fun <Key : Any, Value : Any> empty() = EMPTY as Page<Key, Value>
}
}
}
** 首先LoadResult类被sealed修饰,是个密封类。内部的data class分别是Error和Page。因此,load函数只能返回LoadResult.Error或者LoadResult.Page类型。**
Error好理解,就是封装了下异常信息。Page里面还有prevKey、nextKey就是当前请求的前一个和下一个关键词。可以猜测LoadParam中的key应该就是从这取出的,通常nextKey会作为下次调用load函数中的loadParam.key。data其实就是拉取到的数据。
Page:
- data : 加载到的数据
- prevKey: 前一页的数据,如果当前是第一页那么值为null
- nextKey: 下一页的关键词,如果当前获取到的数据是空,那么该值为null
- itemsBefore: 加载数据之前的可选项目计数。
- itemsAfter: 加载数据后的可选项目计数。
综合上面所说,整个GithubPagingSource如下:
private const val GITHUB_STARTING_PAGE_INDEX = 1
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
val apiQuery = query + IN_QUALIFIER
return try {
val response = service.searchRepos(apiQuery, position, params.loadSize)
val repos = response.items
LoadResult.Page(
data = repos,
prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (repos.isEmpty()) null else position + 1
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
}
其中GithubService就是使用Retrofit进行网络请求的一个类:
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")
suspend fun searchRepos(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") itemsPerPage: Int
): 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())
.build()
.create(GithubService::class.java)
}
}
}
疑问
note:带着如下疑问继续下面研究:
-
上面提到的PageData是什么
-
PageSource中的LoadParam是何时创建的,其中loadSize,key等是如何赋值的
-
LoadParam的子类
Refresh、Prepend、Append是干嘛的 -
LoadResult.Page中的
itemBefore和itemAfter是干嘛的
Pager
前面介绍了PageSource,但是没有说明PageSource在何时应该被怎样调用。下面说的Pager就会用到PageSource并且进行相应配置。看下demo中的代码:
/**
* @author: frc
* @description: 数据仓库
* @date: 2020/10/6 7:29 AM
* @since:
*/
@ExperimentalCoroutinesApi
class GithubRepository(private val service: GithubService) {
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
return Pager(config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false
),
pagingSourceFactory = { GithubPagingSource(service, query) }).flow
}
companion object {
private const val NETWORK_PAGE_SIZE = 50
}
}
GithubRepository是一个仓库类,数据的出口都是在这。我们看到getSearchResultStream方法中使用了Pager,其中的pagingSourceFactory参数就是我们上面自定义的GithubPagingSource,而config参数对应的PagingConfig也有很多熟悉的东西。
package androidx.paging
import kotlinx.coroutines.flow.Flow
/**
* 分页的主要入口;
* [PagingData]的反应流的构造函数。
*每个[PagingData]代表支持分页数据的快照**,对后台数据集的更新应该由一个新的实例[PagingData]表示。
*[PagingSource.invalidate] 并且调用了[AsyncPagingDataDiffer.refresh] 或者 *** [PagingDataAdapter.refresh] 将通知 [Pager] 支持的数据集已经更新了并将生成一个新的[PagingData] / [PagingSource]对来表示更新后的快照。[PagingData]可以在加载时转换数据,并通过`AsyncPagingDataDiffer`或`PagingDataAdapter`在`RecyclerView`中呈现。
*
*/
class Pager<Key : Any, Value : Any>
@JvmOverloads constructor(
config: PagingConfig,
initialKey: Key? = null,
@OptIn(ExperimentalPagingApi::class)
remoteMediator: RemoteMediator<Key, Value>? = null,
pagingSourceFactory: () -> PagingSource<Key, Value>
) {
/**
* A cold [Flow] of [PagingData], which emits new instances of [PagingData] once they become
* invalidated by [PagingSource.invalidate] or calls to [AsyncPagingDataDiffer.refresh] or
* [PagingDataAdapter.refresh].
*/
val flow: Flow<PagingData<Value>> = PageFetcher(
pagingSourceFactory,
initialKey,
config,
remoteMediator
).flow
}
- config : PagingConfig 页面配置
- pagingSourceFactory:()-> PagingSource
PagingConfig
PagingConfig是用于在
Pager中配置在PagingSource中加载内容时的加载行为。上面分析的PagingSource.LoadParam中的loadSize和enablePlaceholders都可以通过PagingConfig配置。
属性:
-
pageSize : 定义了一次从
PagingSource中拉取多少条数据,它的值应该是当前屏幕可现实的倍数。配置页面大小取决于如何加载和使用数据。较小的页面大小可以提高内存使用、延迟,并避免GC搅动。较大的页面通常会在一定程度上提高加载吞吐量(避免一次从SQLite加载超过2MB,因为这会带来额外的成本)。 -
prefetchDistance:预取距离,定义访问从加载内容的边缘触发进一步加载的距离。 通常应该设置为屏幕上可见项目数的几倍。
-
enablePlaceholders:
目前没看到应用场景。还不是很了解 -
initialLoadSize: 第一次从
PagingSource加载数据的条数,通常是比pageSize大的,默认值是pageSize的3倍。 -
maxSize: 定义在应该删除页面之前可以加载到[PagingData]中的项目的最大数量。这个值至少是prefetchDistance*2+pageSize。默认值是无限大,所以页面不会被移除。最小值是2,因为至少加载了2个页面以上才有可能触发页面被移除。注意这里说的额页面是屏幕可见的一页,跟pageSize那个不要混淆。
-
jumpThreshold : 字面意思->跳跃的阀值
目前没看到应用场景。还不是很了解
package androidx.paging
import androidx.annotation.IntRange
import androidx.paging.PagingConfig.Companion.MAX_SIZE_UNBOUNDED
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
/**
* An object used to configure loading behavior within a [Pager], as it loads content from a
* [PagingSource].
*/
class PagingConfig @JvmOverloads constructor(
@JvmField
val pageSize: Int,
@JvmField
@IntRange(from = 0)
val prefetchDistance: Int = pageSize,
@JvmField
val enablePlaceholders: Boolean = true,
@JvmField
@IntRange(from = 1)
val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER,
@JvmField
@IntRange(from = 2)
val maxSize: Int = MAX_SIZE_UNBOUNDED,
@JvmField
val jumpThreshold: Int = COUNT_UNDEFINED
) {
init {
if (!enablePlaceholders && prefetchDistance == 0) {
throw IllegalArgumentException(
"Placeholders and prefetch are the only ways" +
" to trigger loading of more data in PagingData, so either placeholders" +
" must be enabled, or prefetch distance must be > 0."
)
}
if (maxSize != MAX_SIZE_UNBOUNDED && maxSize < pageSize + prefetchDistance * 2) {
throw IllegalArgumentException(
"Maximum size must be at least pageSize + 2*prefetchDist" +
", pageSize=$pageSize, prefetchDist=$prefetchDistance" +
", maxSize=$maxSize"
)
}
require(jumpThreshold == COUNT_UNDEFINED || jumpThreshold > 0) {
"jumpThreshold must be positive to enable jumps or COUNT_UNDEFINED to disable jumping."
}
}
companion object {
@Suppress("MinMaxConstant")
const val MAX_SIZE_UNBOUNDED = Int.MAX_VALUE
internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
}
}