paging3 是 Google 推出的用户加载分页数据的库。但令人意外的是——他没有提供删除相关的接口。在某些场合里,我们就是需要删除数据。我们可以怎么办呢?
本文主要提供了一种我认为的比较合理、高效的方式用于删除 paging3 列表数据项的方法,不涉及 paging3 的其他用法。
搭个简单的测试 APP
PagingSource
data class User(val uid: Long, val name: String)
class NamePagingSource(
private val max: Long
) : PagingSource<Long, User>() {
override fun getRefreshKey(state: PagingState<Long, User>): Long {
return 0
}
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, User> {
val begin = params.key ?: 0
val end = (begin + params.loadSize).coerceAtMost(max)
val data = (begin until end).map {
User(it, "Name@$it")
}
val prevCursor = if (begin == 0L) null else begin
val nextCursor = if (end < max) end else null
return LoadResult.Page(data, prevCursor, nextCursor)
}
}
ViewModel
class UserViewModel : ViewModel() {
companion object {
private const val LIMIT = 100L
private const val PAGE_SIZE = 10
}
private var dataFlow: Flow<PagingData<User>>? = null
fun getDataFlow(): Flow<PagingData<User>> {
return dataFlow ?: Pager(
config = PagingConfig(PAGE_SIZE),
initialKey = 0,
pagingSourceFactory = {
NamePagingSource(LIMIT)
}
).flow.cachedIn(viewModelScope).also {
dataFlow = it
}
}
}
Adapter
class UserListAdapter(
private val deletionCallback: (Int, User) -> Unit
) : PagingDataAdapter<User, UserListAdapter.NameHolder>(COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NameHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = UserListItemBinding.inflate(inflater, parent, false)
return NameHolder(binding)
}
override fun onBindViewHolder(holder: NameHolder, position: Int) {
holder.bind(position, getItem(position)!!)
}
inner class NameHolder(
private val binding: UserListItemBinding
) : RecyclerView.ViewHolder(binding.root), View.OnLongClickListener {
private var index = -1
private var user: User? = null
init {
binding.root.setOnLongClickListener(this)
}
fun bind(index: Int, user: User) {
this.index = index
this.user = user
binding.name.text = user.name
}
override fun onLongClick(v: View): Boolean {
val user = user ?: return false
deletionCallback(index, user)
return true
}
}
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.uid == newItem.uid
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
}
}
layout 很简单,就是一个 TextView:
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#222222"
android:textSize="24sp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
tools:text="ffff"
/>
RecyclerView 的初始化
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
initUserList()
}
private fun initUserList() {
binding.userList.layoutManager = LinearLayoutManager(this)
val adapter = UserListAdapter { position, user ->
// TODO delete it
}
binding.userList.adapter = adapter
lifecycleScope.launch {
viewModel.getDataFlow().collectLatest {
adapter.submitData(it)
}
}
}
}
这样,一个基于 paging3 的简单应用就搭建完成了:
如何删除列表项?
为了删除列表项,首先我们需要拿到当前列表对应的数据。有两种方法可以实现:
adapter.snapshot()返回一个不可修改的列表。我们拷贝一份后并修改后,构造一个新的PagingData然后提交(submitData)给adapter- 缓存一个
PagingData的实例,经由它得到删除后的列表数据
这里我选择的是方法二,这样我们可以不用太关心 PagingData 的实现细节。删除列表项的方法如下:
class MainActivity : AppCompatActivity() {
private lateinit var adapter: UserListAdapter
private lateinit var pagingData: PagingData<User>
// ...
private fun initUserList() {
binding.userList.layoutManager = LinearLayoutManager(this)
binding.userList.adapter = UserListAdapter(::deleteItem).also {
adapter = it
}
lifecycleScope.launch {
viewModel.getDataFlow().collectLatest {
pagingData = it
adapter.submitData(it)
}
}
}
private fun deleteItem(position: Int, user: User) {
lifecycleScope.launch {
pagingData = pagingData.filter { it !== user }
adapter.submitData(pagingData)
}
}
}
效果如下:
性能怎么样?
可能有些同学会觉得,这里直接 filter 效率上太差了但其实不然。filter 的耗时为 Θ(n),ArrayList 的删除耗时为 Ο(n)。最差情况下,两者是相同的;平均而言,filter 会多一倍,但在数量级上是一致的;加之列表数据的项数通常不会非常大,filter 是可接受的。
正确性如何?
正确性包括以下两点:
- 我们删除了某个列表项后,如果重新刷新列表数据,这个被删除的项不应该再出现;
- 我们能够确确实实删除目标。
我们所使用的删除方式的正确是,分别依赖于以下两个事实:
- 客户端展示的列表数据实际上是后端数据的一份拷贝。在需要删除本地这个拷贝的情况下,肯定是由于用户执行了某些操作,导致该列表项无效了。再次从后端拉数据的话,这个列表项也就不会再出现了;
PagingData包含了列表的所有数据(而不是某一页数据)。只有这样,filter才能起到删除的效果。