集成Paging分页
手机应用总免不了要显示列表,例如朋友圈列表,用户列表。话题列表等等,有列表就免不了要分页,分页方案多种多样。现在尝试安卓官方的Android Jetpack的Paging 库。
看了一下文档,内容还是很多的,一时半分无法下手。不管三七二十一,先整一个普通列表。
1.不使用Paging 库做一个RecyclerView普通列表
列表核心控件 SwipeRefreshLayout+RecyclerView
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
列表Item样式
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data />
<LinearLayout
style="@style/defaultSelectableItemBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:orientation="horizontal"
android:padding="0dp">
<ImageView
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:src="@mipmap/ic_launcher"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/tvUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1" />
<TextView
android:id="@+id/tvLastLoginTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上次登录日期" />
</LinearLayout>
</LinearLayout>
</layout>
Fragment
class ChooseUserFragment : BaseFragment<FragmentChooseUserLayoutBinding>() {
private var userAdapter: UserAdapter? = null
override fun getLayoutId() = R.layout.fragment_choose_user_layout
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.recyclerView?.apply {
layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
userAdapter = UserAdapter(requireContext())
adapter = userAdapter
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
queryUsers(1, "测").observe(viewLifecycleOwner) {
if (it is ApiSuccessResponse) {
userAdapter?.addUsers(it.body)
}
}
}
}
}
}
class UserAdapter(val context: Context) : RecyclerView.Adapter<UserViewHolder>() {
private val userList = mutableListOf<UserInfo>()
fun addUsers(users: List<UserInfo>) {
userList.addAll(users)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(
UserViewHolderItemLayoutBinding.bind(
LayoutInflater.from(context).inflate(R.layout.user_view_holder_item_layout, parent, false)
)
)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = userList[holder.absoluteAdapterPosition]
holder.bindData(user)
}
override fun getItemCount(): Int {
return userList.size
}
}
class UserViewHolder(val binding: UserViewHolderItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bindData(user:UserInfo) {
binding.tvUserName.text = user.nickName
binding.tvLastLoginTime.text = TimeUtils.date2String(Date(user.lastLoginTime))
}
}
至此,一个请求接口成功后,显示用户列表的功能基本完成,但是没有下拉刷新,也没分页加载功能。
2.PagingSource
PagingSource<Key, Value> 有两种类型参数:Key 和 Value。键定义了用于加载数据的标识符,值是数据本身的类型。例如,如果您通过将 Int 页码传递给从网络加载各页 User 对象,则应选择 Int 作为 Key 类型,选择 User 作为 Value 类型。
下面的示例实现了按页码加载各页对象的 PagingSource。Key 类型为 Int,Value 类型为 User。
先创建一个UserInfoPagingSource,用于从网络加载用户列表。以page+pageSize方式分页。
class UserInfoPagingSource(val query: String) : PagingSource<Int, UserInfo>() {
private val LogTag = "UserInfoPagingSource"
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UserInfo> {
val nextPageNumber = params.key ?: 1
LogUtils.d(LogTag,"load nextPageNumber:$nextPageNumber\tloadSize:${params.loadSize}")
var result: LoadResult<Int, UserInfo>? = null
queryUsers(nextPageNumber, query, params.loadSize)
.asFlow()
.catch { e ->
LogUtils.d(LogTag, "catch e:$e")
result = LoadResult.Error(e)
}.onCompletion { t ->
LogUtils.d(LogTag, "onCompletion t:$t")
}.take(1)
.collect { response ->
LogUtils.d(LogTag, "collect response:$response")
if (response is ApiSuccessResponse) {
result = LoadResult.Page(
data = response.body,
prevKey = null,
nextKey = if (response.body.isEmpty()) null else nextPageNumber + 1
)
} else if (response is ApiErrorResponse) {
result = LoadResult.Error(RuntimeException(response.errorMessage))
} else {
result = LoadResult.Invalid()
}
}
return result ?: LoadResult.Invalid()
}
override fun getRefreshKey(state: PagingState<Int, UserInfo>): Int? {
// Try to find the page key of the closest page to anchorPosition, from
// either the prevKey or the nextKey, but you need to handle nullability
// here:
// * prevKey == null -> anchorPage is the first page.
// * nextKey == null -> anchorPage is the last page.
// * both prevKey and nextKey null -> anchorPage is the initial page, so
// just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
3.ViewModel
接下来,您需要来自 PagingSource 实现的分页数据流。通常,您应在 ViewModel 中设置数据流。Pager 类提供的方法可显示来自 PagingSource 的 PagingData 对象的响应式流。Paging 库支持使用多种流类型,包括 Flow、LiveData 以及 RxJava 中的 Flowable 和 Observable 类型。
当您创建 Pager 实例来设置响应式流时,必须为实例提供 PagingConfig 配置对象和告知 Pager 如何获取 PagingSource 实现实例的函数:
class ChooseUserViewModel : ViewModel() {
val loadUserFlow = Pager(PagingConfig(pageSize = 15, initialLoadSize = 15)) {
UserInfoPagingSource(query = "测")
}.flow
.cachedIn(viewModelScope)
}
4.PagingDataAdapter
然后修改之前写好的RecyclerView.Adapter,主要修改之处,是从继承RecyclerView.Adapter变成继承PagingDataAdapter,因为
PagingDataAdapter需要一个diffCallback,所以还需要一个单例对象UserComparator
class UserAdapter(val context: Context) : PagingDataAdapter<UserInfo, UserViewHolder>(diffCallback = UserComparator) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(
UserViewHolderItemLayoutBinding.bind(
LayoutInflater.from(context).inflate(R.layout.user_view_holder_item_layout, parent, false)
)
)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = getItem(position)
holder.bindData(user)
}
}
//单例对象
object UserComparator : DiffUtil.ItemCallback<UserInfo>() {
override fun areItemsTheSame(oldItem: UserInfo, newItem: UserInfo): Boolean {
// Id is unique.
return oldItem.objectId == newItem.objectId
}
override fun areContentsTheSame(oldItem: UserInfo, newItem: UserInfo): Boolean {
return oldItem == newItem
}
}
5.列表数据更新
主要变化之处是,自字定的继承RecyclerView.Adapter的适配器,需要自己定义一个成员变量List<UserInfo>保存数据,然后还需要一个列表加载下一页时增加数据的方法并且notifyDataSetChanged,例如
private val userList = mutableListOf<UserInfo>()
fun addUsers(users: List<UserInfo>) {
userList.addAll(users)
notifyDataSetChanged()
}
Adapter 继承自PagingDataAdapter后就不需要了。paging库会自动更新数据列表
更新数据方法,从调用自定义的addUsers方法,变成userAdapter?.submitData(pagingData)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
....省略一些代码...s
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
chooseUserViewModel.loadUserFlow.collect{ pagingData->
userAdapter?.submitData(pagingData)
}
}
}
}
到这一个时候,一个会自动分页的列表已经做好了。但是没有下拉刷新,也没有加载失败提示。
6.添加header,footer,和分隔线
实现添加添加header,footer,和分隔线,其实就是转换数据流的知识点。可以对流实现filter 过滤,map转换等中间操作。
viewModel里的流添加map操作
class ChooseUserViewModel : ViewModel() {
val loadUserFlow = Pager(PagingConfig(pageSize = 15, initialLoadSize = 15)) {
UserInfoPagingSource(query = "测")
}.flow.map { pagingData ->
pagingData.map { user ->
UiModel.UserModel(user)
}.insertSeparators { before, after ->
when {
before == null -> null
after == null -> UiModel.SeparatorModel(UiModel.FOOTER, before = before, after = after)
else -> UiModel.SeparatorModel(description = UiModel.ITEM_DECORATION, before = before, after = after)
}
}
}.cachedIn(viewModelScope)
}
相应的UserComparator和UserAdapter也需要相应的修改。
sealed class UiModel {
data class UserModel(val user: UserInfo) : UiModel()
data class SeparatorModel(val description: String, val before: UserModel?, val after: UserModel?) : UiModel()
companion object {
const val FOOTER = "footer"
const val ITEM_DECORATION = "itemDecoration"
}
}
class UserAdapter(val context: Context) : PagingDataAdapter<UiModel, RecyclerView.ViewHolder>(diffCallback = UserComparator) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewHolder = when (viewType) {
R.layout.user_view_holder_item_layout -> UserViewHolder(
UserViewHolderItemLayoutBinding.bind(
LayoutInflater.from(context).inflate(R.layout.user_view_holder_item_layout, parent, false)
)
)
R.layout.user_view_holder_footer_item_layout -> FooterViewHolder(
UserViewHolderFooterItemLayoutBinding.bind(
LayoutInflater.from(context).inflate(R.layout.user_view_holder_footer_item_layout, parent, false)
)
)
R.layout.user_view_holder_item_decoration_layout -> ItemDecorationViewHolder(
LayoutInflater.from(context).inflate(R.layout.user_view_holder_item_decoration_layout, parent, false)
)
else -> UserViewHolder(
UserViewHolderItemLayoutBinding.bind(
LayoutInflater.from(context).inflate(R.layout.user_view_holder_item_layout, parent, false)
)
)
}
return viewHolder
}
override fun getItemViewType(position: Int): Int {
val uiModel = peek(position)
return if (uiModel is UiModel.UserModel) {
R.layout.user_view_holder_item_layout
} else if (uiModel is UiModel.SeparatorModel && uiModel.description == UiModel.FOOTER) {
R.layout.user_view_holder_footer_item_layout
} else if (uiModel is UiModel.SeparatorModel && uiModel.description == UiModel.ITEM_DECORATION) {
R.layout.user_view_holder_item_decoration_layout
} else {
super.getItemViewType(position)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val itemData = getItem(position)
when (holder) {
is UserViewHolder -> {
val user = (itemData as? UiModel.UserModel)?.user
holder.bindData(user)
holder.itemView.setOnClickListener {
ToastUtils.showShort("userId:${user?.objectId}")
}
}
}
}
}
//单例对象
object UserComparator : DiffUtil.ItemCallback<UiModel>() {
override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {
// Id is unique.
val isSameUserItem =
oldItem is UiModel.UserModel && newItem is UiModel.UserModel && oldItem.user.objectId == newItem.user.objectId
val isSameSeparatorItem =
oldItem is UiModel.SeparatorModel && newItem is UiModel.SeparatorModel && oldItem == newItem
return isSameUserItem || isSameSeparatorItem
}
override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {
return oldItem == newItem
}
}
class UserViewHolder(val binding: UserViewHolderItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bindData(user: UserInfo?) {
binding.tvUserName.text = user?.nickName
binding.tvLastLoginTime.text = TimeUtils.date2String(Date(user?.lastLoginTime ?: 0))
}
}
class FooterViewHolder(val binding: UserViewHolderFooterItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bindData(user: UserInfo?) {
}
}
class ItemDecorationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
}
7.首次加载loading
如果需要实现首次加载时的loading,需要监听到加载状态变化,所以需要用到加载状态PagingDataAdapter的loadStateFlow来实现
加载状态监听回调有两种
一种是addLoadStateListener
pagingDataAdapter?.addLoadStateListener {
}
一种是流收集
// Activities can use lifecycleScope directly, but Fragments should instead use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
pagingAdapter.loadStateFlow.collectLatest { loadStates ->
progressBar.isVisible = loadStates.refresh is LoadState.Loading
retry.isVisible = loadState.refresh !is LoadState.Loading
errorMsg.isVisible = loadState.refresh is LoadState.Error
}
}
推荐是第二种,因为流有很多中间操作符,可以很方便的处理一些中间状态,例如distinctUntilChangedBy,filter
lifecycleScope.launchWhenCreated {
adapter.loadStateFlow
// Only emit when REFRESH LoadState for RemoteMediator changes.
.distinctUntilChangedBy { it.refresh }
// Only react to cases where REFRESH completes, such as NotLoading.
.filter { it.refresh is LoadState.NotLoading }
// Scroll to top is synchronous with UI updates, even if remote load was
// triggered.
.collect { binding.list.scrollToPosition(0) }
}
当this.refresh is LoadState.Loading时,其他都是 LoadState.NotLoading时,即为下拉刷新或者首次加载,配合adapter counter即可以判断出是否是下拉刷新状态还是首次加载状态,
fun CombinedLoadStates.isRefreshing() =
this.append is LoadState.NotLoading
&& this.refresh is LoadState.Loading
&& this.prepend is LoadState.NotLoading
8.实现footer加载状态
前面实现的footer,只会在页面加载完成后提示,并不会在每一次加载下一页时提示,
定义并继承LoadStateAdapter
class UserLoadStateAdapter(val context: Context, private val retry: () -> Unit) : LoadStateAdapter<FooterViewHolder>() {
override fun onBindViewHolder(holder: FooterViewHolder, loadState: LoadState) {
holder.bindingLoadState(loadState)
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): FooterViewHolder {
return FooterViewHolder(
UserViewHolderFooterItemLayoutBinding.bind(
LayoutInflater.from(context).inflate(R.layout.user_view_holder_footer_item_layout, parent, false)
)
)
}
然后使用withLoadStateHeaderAndFooter或者withLoadStateFooter``withLoadStateHeader生成新的Adapter赋值给RecyclerView的adapter
binding.list.adapter = adapter.withLoadStateHeaderAndFooter(
header = PostsLoadStateAdapter(adapter),
footer = PostsLoadStateAdapter(adapter)
)
9.失败重试和下拉刷新或者首次加载失败刷新
PagingDataAdapter提供了几个方法,用于重试或者刷新
/**
* Retry any failed load requests that would result in a [LoadState.Error] update to this
* [PagingDataAdapter].
*
* Unlike [refresh], this does not invalidate [PagingSource], it only retries failed loads
* within the same generation of [PagingData].
*
* [LoadState.Error] can be generated from two types of load requests:
* * [PagingSource.load] returning [PagingSource.LoadResult.Error]
* * [RemoteMediator.load] returning [RemoteMediator.MediatorResult.Error]
*/
fun retry() {
differ.retry()
}
/**
* Refresh the data presented by this [PagingDataAdapter].
*
* [refresh] triggers the creation of a new [PagingData] with a new instance of [PagingSource]
* to represent an updated snapshot of the backing dataset. If a [RemoteMediator] is set,
* calling [refresh] will also trigger a call to [RemoteMediator.load] with [LoadType] [REFRESH]
* to allow [RemoteMediator] to check for updates to the dataset backing [PagingSource].
*
* Note: This API is intended for UI-driven refresh signals, such as swipe-to-refresh.
* Invalidation due repository-layer signals, such as DB-updates, should instead use
* [PagingSource.invalidate].
*
* @see PagingSource.invalidate
*
* @sample androidx.paging.samples.refreshSample
*/
fun refresh() {
differ.refresh()
}
10.数据先加载到数据库然后才加载到界面
可能通过继承RemoteMediator实现
@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
private val query: String,
private val database: RoomDb,
private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
val userDao = database.userDao()
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, User>
): MediatorResult {
// ...
}
}
val userDao = database.userDao()
val pager = Pager(
config = PagingConfig(pageSize = 50)
remoteMediator = ExampleRemoteMediator(query, database, networkService)
) {
userDao.pagingSource(query)
}
暂时没有用到,后面用到再详细看。