paging3 如何删除列表项

3,485 阅读3分钟

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 的简单应用就搭建完成了:

如何删除列表项?

为了删除列表项,首先我们需要拿到当前列表对应的数据。有两种方法可以实现:

  1. adapter.snapshot() 返回一个不可修改的列表。我们拷贝一份后并修改后,构造一个新的 PagingData 然后提交(submitData)给 adapter
  2. 缓存一个 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 是可接受的。

正确性如何?

正确性包括以下两点:

  1. 我们删除了某个列表项后,如果重新刷新列表数据,这个被删除的项不应该再出现;
  2. 我们能够确确实实删除目标。

我们所使用的删除方式的正确是,分别依赖于以下两个事实:

  1. 客户端展示的列表数据实际上是后端数据的一份拷贝。在需要删除本地这个拷贝的情况下,肯定是由于用户执行了某些操作,导致该列表项无效了。再次从后端拉数据的话,这个列表项也就不会再出现了;
  2. PagingData 包含了列表的所有数据(而不是某一页数据)。只有这样,filter 才能起到删除的效果。