使用paging3.0实现数据源分页的代码实例
paging3.0一个简单的例子
本文使用kotlin,如果你使用java,那么官网有对应的java代码,建议学习官方文档,效果更佳
基于 :
def paging_version = "3.0.0-alpha03"
implementation "androidx.paging:paging-runtime:$paging_version"
是的,没有其他的库,我们可以专注于paging,真是一个简单的例子。
先看下实现的效果,footer在paging3是可以自定义的,gif就弄了个加载完成的,实际上还有其他状态,加载完毕/加载错误这些状态都有。
下面开始正文,算了,,,,, 明天再写吧,都12点了,没错我罢工了 睡觉。。。。
接着昨天继续!
定义数据源 从网络或数据库中异步获取数据
定义类继承PagingSource,
class ExamplePagingSource(private val backend: ExampleBackendService
) : PagingSource<Int, User>() {
override suspend fun load( params: LoadParams<Int> ): LoadResult<Int, User> {
return try {
//从后台或数据库异步获取数据
val response = backend.searchUsers()
var nextKey = response.nextKey
//组成成功
LoadResult.Page(
data = response.response,
prevKey = null, // Only paging forward.
nextKey = if (nextKey!! >= 100) null else nextKey
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
PagingSource的两个泛型Key和Value分别代表加载数据的标识符和数据本身的类型,目前我理解Key可以是任意类型,不为null即可,为null表示没有下一页数据了。load函数只有两个结果,成功or失败,对应LoadResult.Page/ LoadResult.Error,它是一个挂起函数,参数LoadParams是加载请求的参数,包括配置的每页加载数量等,自己点进去看下注释吧,searchUsers方法是我模拟异步返回数据,下面贴出代码。
class ExampleBackendService {
private var count = 0
suspend fun searchUsers(): Response {
Log.d("duo_shine", "发起请求 模拟耗时操作")
return withContext(Dispatchers.IO) {
delay(2000)
val data = ArrayList<User>()
for (id in count until count + 20) {
data.add(User(name = id.toString()))
}
count += 20
Log.d("duo_shine", "加载完成 返回数据")
Response(data, count)
}
}
}
这里模拟后台请求数据,做了一个子线程2000的耗时,Response 是实体类,
data class User(val name: String)
data class Response(val response: List<User>, val nextKey: Int?)
创建数据流
private val flow = Pager(PagingConfig(pageSize = 20)) {
ExamplePagingSource(ExampleBackendService())
}.flow.cachedIn(lifecycleScope)
官网推荐的是将流写在ViewModel,这可以确保,在配置更改(例如旋转)时,新的Activity将接收现有的数据立即取回,而不是再重新加载。我这里直接在Activity里用的,我想保持这个例子更简单,注意PagingConfig中还有一些其他的配置参数可以写入,比如预加载下一页的距离等等其他配置项
目前数据域源准备好了,下面创建适配器
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) :
PagingDataAdapter<User, UserAdapter.UserViewHolder>(diffCallback) {
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var tvName:TextView? = null
init {
tvName = itemView.findViewById(R.id.tvName)
}
fun bind(item: User?) {
tvName?.text = item?.name
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.adapter_user_item, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val item = getItem(position)
// Note that item may be null. ViewHolder must support binding a
// null item as a placeholder.
holder.bind(item)
}
object UserComparator : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
// Id is unique.
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
}
布局简单
<TextView
android:layout_width="match_parent"
android:padding="10dp"
android:id="@+id/tvName"
android:layout_height="wrap_content" />
现在创建适配器给recyclerView
val pagingAdapter = UserAdapter(UserAdapter.UserComparator)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = pagingAdapter
lifecycleScope.launch {
flow.collectLatest { pagingData ->
pagingAdapter.submitData(pagingData)
}
}
现在运行吧。通过logcat你可以看到目前的加载情况,当然看不到footer那种加载成功或失败的效果,下面添加footer和header
/**
*添加footer
*/
val withLoadStateFooter =
pagingAdapter.withLoadStateFooter(footer = ExampleLoadStateAdapter {
//请求出错后重试
Log.d("duo_shine", "retry")
})
recyclerView.adapter = withLoadStateFooter
修改recyclerView的adapter赋值,目前我们只增加了一个footer,实际上PagingDataAdapter支持header和footer,这里用不到就不添加header了,下面创建ExampleLoadStateAdapter
class LoadStateViewHolder(
parent: ViewGroup,
retry: () -> Unit
) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.load_state_item, parent, false)
) {
private val binding = LoadStateItemBinding.bind(itemView)
private val progressBar: ProgressBar = binding.progressBar
private val errorMsg: TextView = binding.errorMsg
private val noData: TextView = binding.noData
private val retry: Button = binding.retryButton
.also {
it.setOnClickListener { retry() }
}
fun bind(loadState: LoadState) {
if (loadState is LoadState.Error) {
val msg = loadState.error.localizedMessage
errorMsg.text = msg
}
progressBar.isVisible = loadState is LoadState.Loading
//其他状态都不需要显示
noData.isVisible = false
retry.isVisible = loadState is LoadState.Error
errorMsg.isVisible = loadState is LoadState.Error
}
}
// Adapter that displays a loading spinner when
// state = LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter(
private val retry: () -> Unit
) : LoadStateAdapter<LoadStateViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
loadState: LoadState
) = LoadStateViewHolder(parent, retry)
override fun onBindViewHolder(
holder: LoadStateViewHolder,
loadState: LoadState
) = holder.bind(loadState)
}
布局文件也是从官网的demo里拷出来的
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2017 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/error_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
<TextView
android:id="@+id/no_data"
android:text="别拉了 已经到底了"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<Button
android:id="@+id/retry_button"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="重试" />
</LinearLayout>
对了,使用了视图绑定的库,启用它你只需要在build.gradle添加
android {
viewBinding {
enabled = true
}
....
我看没有数据加载完的ui,我给增加了一个控件,但是我发现LoadState中没有一个好的状态去支持它,这个最后再讲,目前运行起来,下拉时就会有加载进度条显示出来,如果你觉得在一个layout中显示和隐藏控件不酷的话可以重写getStateViewType方法来支持不同的ViewHolder,下面说下我怎么去显示数据已加载完毕,类似下面的效果
代码如下 ,还是之前的代码,重点就是中间那几行,当从后台得知已没有更多数据可加载时我们可以直接返回一个指定的错误
try {
//从后台或数据库异步获取数据
val response = backend.searchUsers()
var nextKey = response.nextKey
//当已经没有下一页数据时返回指定的错误 表示数据已加载完毕
if (nextKey!! >= 100) {
return LoadResult.Error(NoMoreException("加载完成"))
}
//组成成功
return LoadResult.Page(
data = response.response,
prevKey = null, // Only paging forward.
nextKey = if (nextKey!! >= 100) null else nextKey
)
在处理loadState的状态时我们就可以判断异常的类型
fun bind(loadState: LoadState) {
if (loadState is LoadState.Error) {
val error = loadState.error
//当收到指定异常时表示数据已加载完毕 给用户一些提示
if (error is NoMoreException) {
progressBar.isVisible = loadState is LoadState.Loading
noData.isVisible = true
retry.isVisible = false
errorMsg.isVisible = loadState is LoadState.Error
return
}
errorMsg.text = error.localizedMessage
}
progressBar.isVisible = loadState is LoadState.Loading
//除了NoMoreException其他状态都不需要显示
noData.isVisible = false
retry.isVisible = loadState is LoadState.Error
errorMsg.isVisible = loadState is LoadState.Error
}
目前paging3.0还是Alpha版本,功能还没有完善,等正式版本出来之后我再更新本文。
本文地址:https://blog.csdn.net/duo_shine/article/details/107723351
推荐阅读