14、DataBinding与Paging3:PagingData与DataBinding、PagingDataAdapter的绑定、加载状态显示、分页刷新处理
说实话,Paging3 刚出来那会儿,我第一反应是「又来个新东西,学不动了」。但真正用上之后,我发现它跟 DataBinding 简直是天生一对。你想想看,一个负责数据分页加载,一个负责自动更新 UI,配合起来代码量能少一半。
这一章,我们就来聊聊怎么把 Paging3 和 DataBinding 揉在一起用。我会把我在项目中踩过的坑、总结的经验都抖出来。
14.1 PagingData 与 DataBinding 的绑定思路
先理清一个核心问题:PagingData 是异步流,DataBinding 是同步绑定。怎么让它们和谐共处?
我的做法是——在 ViewModel 里把 PagingData 转成 LiveData,然后让 DataBinding 去观察这个 LiveData。这样既保留了 Paging3 的分页能力,又兼容了 DataBinding 的自动更新机制。
来看代码:
class UserViewModel : ViewModel() {
// 1. 定义 PagingData 的 LiveData
private val _users = MutableLiveData<PagingData<User>>()
val users: LiveData<PagingData<User>> get() = _users
// 2. 创建 PagingSource
class UserPagingSource : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
val page = params.key ?: 1
val response = ApiService.getUsers(page, params.loadSize)
LoadResult.Page(
data = response.data,
prevKey = if (page > 1) page - 1 else null,
nextKey = if (response.data.isNotEmpty()) page + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
// 3. 启动分页加载
fun loadUsers() {
viewModelScope.launch {
Pager(PagingConfig(pageSize = 20)) {
UserPagingSource()
}.flow.cachedIn(viewModelScope).collect {
_users.value = it
}
}
}
}
这里有个细节:cachedIn(viewModelScope) 一定要加。我之前没加,结果每次屏幕旋转都重新请求网络,用户数据刷得飞起,体验极差。
14.2 PagingDataAdapter 与 DataBinding 的绑定
PagingDataAdapter 是 Paging3 提供的专用 Adapter,它知道怎么处理 PagingData 的增量更新。但问题来了——它默认不支持 DataBinding。
嗯,这里需要我们自己动手封装一下。
我的做法是写一个基类,让 Adapter 的 ViewHolder 支持 DataBinding:
abstract class BindingPagingDataAdapter<T, V : ViewDataBinding>(
private val diffCallback: DiffUtil.ItemCallback<T>
) : PagingDataAdapter<T, BindingPagingDataAdapter<T, V>.BindingViewHolder>(diffCallback) {
inner class BindingViewHolder(val binding: V) : RecyclerView.ViewHolder(binding.root)
override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
val item = getItem(position)
item?.let { bind(holder.binding, it, position) }
holder.binding.executePendingBindings()
}
protected abstract fun bind(binding: V, item: T, position: Int)
}
然后具体的 Adapter 就清爽多了:
class UserAdapter : BindingPagingDataAdapter<User, ItemUserBinding>(UserDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
val binding = ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingViewHolder(binding)
}
override fun bind(binding: ItemUserBinding, item: User, position: Int) {
binding.user = item
binding.executePendingBindings()
}
}
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
你看,Adapter 里只需要关心怎么绑定数据,分页逻辑全交给 PagingDataAdapter 处理了。
14.3 加载状态显示
分页加载时,用户需要知道当前状态:是加载中、加载成功还是加载失败。Paging3 提供了 CombinedLoadStates,我们可以用它来驱动 UI 状态。
我在项目中是这样做的——在 Fragment 里监听加载状态,然后通过 DataBinding 更新 UI:
class UserFragment : Fragment() {
private lateinit var binding: FragmentUserBinding
private val viewModel: UserViewModel by viewModels()
private lateinit var adapter: UserAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentUserBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = UserAdapter()
binding.recyclerView.adapter = adapter
// 监听加载状态
viewModel.users.observe(viewLifecycleOwner) { pagingData ->
adapter.submitData(lifecycle, pagingData)
}
// 监听加载状态变化
adapter.addLoadStateListener { loadState ->
when (loadState.refresh) {
is LoadState.Loading -> {
binding.isLoading = true
binding.errorMessage = null
}
is LoadState.Error -> {
binding.isLoading = false
binding.errorMessage = (loadState.refresh as LoadState.Error).error.message
}
is LoadState.NotLoading -> {
binding.isLoading = false
binding.errorMessage = null
}
}
}
}
}
布局文件里,我用 DataBinding 来控制加载和错误视图的显示:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="isLoading" type="Boolean" />
<variable name="errorMessage" type="String" />
</data>
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="@{isLoading ? View.VISIBLE : View.GONE}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@{errorMessage}"
android:visibility="@{errorMessage != null ? View.VISIBLE : View.GONE}" />
</LinearLayout>
</layout>
refresh 和 append。refresh 是首次加载或刷新,append 是加载更多。我一般只监听 refresh 来显示全屏加载状态,append 用底部加载更多指示器。
14.4 分页刷新处理
下拉刷新是分页列表的标配。Paging3 配合 SwipeRefreshLayout 用起来很顺手。
我的做法是在 ViewModel 里暴露一个刷新方法:
class UserViewModel : ViewModel() {
private val refreshTrigger = MutableStateFlow(0)
val users: StateFlow<PagingData<User>> = refreshTrigger
.flatMapLatest {
Pager(PagingConfig(pageSize = 20)) {
UserPagingSource()
}.flow.cachedIn(viewModelScope)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), PagingData.empty())
fun refresh() {
refreshTrigger.value++
}
}
Fragment 里绑定 SwipeRefreshLayout:
binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.refresh()
}
// 监听刷新完成
adapter.addLoadStateListener { loadState ->
binding.swipeRefreshLayout.isRefreshing = loadState.refresh is LoadState.Loading
}
这里有个坑:refreshTrigger.value++ 会触发新的 PagingData 流,但旧的流还在。如果不加 cachedIn(viewModelScope),旧的流会泄漏。我之前就因为这个吃过亏,内存占用一直涨。
14.5 知识体系总览
为了让你更直观地理解这一章的内容,我画了一张图:
14.6 避坑指南
最后,分享几个我踩过的坑:
- PagingData 不能重复使用:每个 PagingData 只能被 submitData 一次。如果需要刷新,必须创建新的 PagingData 流。
- Lifecycle 绑定:
adapter.submitData(lifecycle, pagingData)一定要传 lifecycle,否则页面不可见时还在加载数据,浪费流量。 - DiffUtil 的 equals 方法:如果数据类用了 data class,DiffUtil 的 areContentsTheSame 默认比较所有字段。如果字段太多,建议只比较关键字段,提升性能。
- 空列表处理:PagingData 为空时,loadState.refresh 是 NotLoading,但列表是空的。需要单独判断
adapter.itemCount == 0来显示空视图。
flatMapLatest 才解决——它会自动取消旧的流。
好了,这一章的内容就到这里。DataBinding 和 Paging3 的组合拳打好了,分页列表的开发效率能提升一大截。下一章我们聊聊更高级的用法——DataBinding 与 Room 数据库的配合,敬请期待。
公众号:蓝海资料掘金营,微信deep3321