paging3 如何删除列表项
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
才能起到删除的效果。
作者:Jekton
链接:https://juejin.cn/post/7048520991193956389