Android Jetpack分页库 Paging3 简单使用

简介

Paging 库可帮助您加载和显示来自本地存储或网络中更大的数据集中的数据页面。此方法可让您的应用更高效地利用网络带宽和系统资源。Paging 库的组件旨在契合推荐的 Android 应用架构,流畅集成其他 Jetpack 组件,并提供一流的 Kotlin 支持。

使用 Paging 库的优势

Paging 库包含以下功能:

  • 分页数据的内存中缓存。该功能可确保您的应用在处理分页数据时高效利用系统资源。
  • 内置的请求重复信息删除功能,可确保您的应用高效利用网络带宽和系统资源。
  • 可配置的 RecyclerView 适配器,会在用户滚动到已加载数据的末尾时自动请求数据。
  • 对 Kotlin 协程和 Flow 以及 LiveData 和 RxJava 的一流支持。
  • 内置对错误处理功能的支持,包括刷新和重试功能。

设置

如需将 Paging 组件导入到 Android 应用中,请将以下依赖项添加到应用的 build.gradle 文件中:

1
2
3
4
5
6
dependencies {
def paging_version = "3.1.1"

// Paging 分页库:https://developer.android.google.cn/jetpack/androidx/releases/paging
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
}

库的架构

Paging 库直接集成到推荐的 Android 应用架构中。该库的组件在应用的三个层运行:

  • 代码库层
  • ViewModel
  • 界面层

图片描述


Tips:以上来自官方文档介绍,传送门

CodeLab

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def paging_version = "3.1.1"
def refresh_version = "1.0.0"
def retrofit_version = "2.9.0"
def gson_version = "2.8.8"

// Paging 分页库:https://developer.android.google.cn/jetpack/androidx/releases/paging
implementation "androidx.paging:paging-runtime-ktx:$paging_version"

// 下拉刷新框架(可选)
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$refresh_version"

// 使用 Android ViewBinding 更简单:https://github.com/androidbroadcast/ViewBindingPropertyDelegate(可选)
implementation 'com.github.kirich1409:viewbindingpropertydelegate:1.5.3'

// Json 解析框架:https://github.com/google/gson
implementation "com.google.code.gson:gson:$gson_version"

// Android网络请求库:https://github.com/square/retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

配置 OkHttp 和 Retrofit

ServiceCreator.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/10/02
* desc : 网络请求服务创建者
*/
object ServiceCreator {

private const val BASE_URL = "https://api.sunofbeaches.com/"
private val client = OkHttpClient.Builder()
.build()

val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()

inline fun <reified T> create(): T = retrofit.create(T::class.java)
}

创建通用结果实体类

ApiResponse.kt

1
2
3
4
5
6
7
8
9
10
11
data class ApiResponse<T>(
@SerializedName("code")
val code: Int,
@SerializedName("data")
val `data`: T,
@SerializedName("message")
val message: String,
@SerializedName("success")
val success: Boolean
)

创建通用分页实体类

Page.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
open class Page<T> {
@SerializedName("currentPage")
val currentPage: Int = 0

@SerializedName("hasNext")
val hasNext: Boolean = false

@SerializedName("hasPre")
val hasPre: Boolean = false

@SerializedName("list")
val list: List<T> = listOf()

@SerializedName("pageSize")
val pageSize: Int = 0

@SerializedName("total")
val total: Int = 0

@SerializedName("totalPage")
val totalPage: Int = 0
}

创建文章实体类

UserArticle.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/10/31
* desc : 用户文章列表
*/
class UserArticle : Page<UserArticle.UserArticleItem>() {
data class UserArticleItem(
@SerializedName("articleType")
val articleType: String,
@SerializedName("avatar")
val avatar: String,
@SerializedName("covers")
val covers: List<String>,
@SerializedName("createTime")
val createTime: String,
@SerializedName("id")
val id: String,
@SerializedName("labels")
val labels: List<String>,
@SerializedName("nickname")
val nickname: String,
@SerializedName("state")
val state: String,
@SerializedName("thumbUp")
val thumbUp: Int,
@SerializedName("title")
val title: String,
@SerializedName("userId")
val userId: String,
@SerializedName("viewCount")
val viewCount: Int,
@SerializedName("vip")
val vip: Boolean
)
}

编写网络请求接口

ArticleApi.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface ArticleApi {

/**
* 获取指定用户的文章列表
*/
@GET("ct/article/list/{userId}/{page}")
suspend fun loadArticleListByUserId(
@Path("userId") userId: String,
@Path("page") page: Int
): ApiResponse<UserArticle>

companion object : ArticleApi by ServiceCreator.create()
}

编写 PagingSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/10/31
* desc : 指定用户的文章 PagingSource
*/
class UserArticlePagingSource(private val userId: String) :
PagingSource<Int, UserArticle.UserArticleItem>() {

override fun getRefreshKey(state: PagingState<Int, UserArticle.UserArticleItem>): Int? = null

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UserArticle.UserArticleItem> {
return try {
val page = params.key ?: FIRST_PAGE_INDEX
Timber.d("load:===> userId is $userId page is $page")
val response = ArticleApi.loadArticleListByUserId(userId = userId, page = page)
val responseData = response.data
val currentPage = responseData.currentPage
val prevKey = if (responseData.hasPre) currentPage - 1 else null
val nextKey = if (responseData.hasNext) currentPage + 1 else null
if (response.success) LoadResult.Page(
data = responseData.list,
prevKey = prevKey,
nextKey = nextKey
)
else LoadResult.Error(RuntimeException(response.message))
} catch (t: Throwable) {
t.printStackTrace()
LoadResult.Error(t)
}
}

companion object {

// 第一页
private const val FIRST_PAGE_INDEX = 1
}
}

编写 ArticleViewModel

ArticleViewModel.kt

1
2
3
4
5
6
7
8
9
10
class ArticleViewModel: ViewModel() {

fun getArticleListByUserId(userId: String): Flow<PagingData<UserArticle.UserArticleItem>> {
return Pager(config = PagingConfig(30),
pagingSourceFactory = {
UserArticlePagingSource(userId)
}).flow.cachedIn(viewModelScope)
}
}

创建 article_item

article_item.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
android:paddingHorizontal="20dp"
android:paddingVertical="30dp"
android:singleLine="true" />

</LinearLayout>

创建 ArticleAdapter 适配器及 DiffCallback

ArticleAdapter.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ArticleAdapter : PagingDataAdapter<UserArticle.UserArticleItem, ArticleAdapter.ViewHolder>(diffCallback = diffCallback) {

inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

private val mBinding by viewBinding<ArticleItemBinding>()

fun binding(item: UserArticle.UserArticleItem?) {
item ?: return
mBinding.apply {
tvTitle.text = item.title
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.article_item, parent, false)
return ViewHolder(itemView)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding(getItem(position))
}

companion object {

private val diffCallback = object : DiffUtil.ItemCallback<UserArticle.UserArticleItem>() {

override fun areItemsTheSame(oldItem: UserArticle.UserArticleItem, newItem: UserArticle.UserArticleItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: UserArticle.UserArticleItem, newItem: UserArticle.UserArticleItem): Boolean {
return oldItem == newItem
}
}
}
}

创建 paging_activity 布局

paging_activity.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".PagingActivity">

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

</LinearLayout>

创建 PagingActivity

PagingActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class PagingActivity : AppCompatActivity(R.layout.paging_activity) {

private val mBinding by viewBinding<PagingActivityBinding>()
private val mArticleViewModel by viewModels<ArticleViewModel>()
private val mArticleAdapter = ArticleAdapter()
private val mLoadStateListener: (CombinedLoadStates) -> Unit = {
mBinding.refreshLayout.isRefreshing = when (it.refresh) {
// 可显示加载中状态
is LoadState.Loading -> true
// 加载结束状态
is LoadState.NotLoading -> false
// 可显示加载错误状态
is LoadState.Error -> false
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initView()
initData()
initEvent()
}

private fun initView() {
mBinding.apply {
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = mArticleAdapter
}
}
mArticleAdapter.addLoadStateListener(mLoadStateListener)
}

private fun initData() {
// 在 Lifecycle.State.CREATED ---> Lifecycle.State.DESTROYED 之间运行,当生命周期被销毁时,该 Job 将被自动取消。
lifecycleScope.launchWhenCreated {
mArticleViewModel.getArticleListByUserId("1204736502274318336").collectLatest {
// 提交数据
mArticleAdapter.submitData(it)
}
}
}

private fun initEvent() {
// 调用适配器的刷新方法
mBinding.refreshLayout.setOnRefreshListener { mArticleAdapter.refresh() }
}

override fun onDestroy() {
super.onDestroy()
mArticleAdapter.removeLoadStateListener(mLoadStateListener)
}
}

效果图

图片描述

总结

  • 适配器需要继承自 PagingDataAdapter
  • 需要编写 DiffUtil.ItemCallback (用于 item 项的差异比较)

总的来说,只要按照 CodeLab 这部分的流程来写,如果使用 Paging3 来简单的展示分页列表其实还是很简单的。

如果对你有帮助的话,欢迎一键三连+关注哦~


Android Jetpack分页库 Paging3 简单使用
https://blog.funkt.cn/2022/11/04/1588532931388956674/
作者
anjiemo
发布于
2022年11月4日
许可协议
未经作者授权,禁止转载