我使用以下方法制作了一个快速样本DiffUtil.Callback
and ListAdapter<T, K>
(所以我在适配器上调用了submitList(...)),并且没有任何问题。
然后我将适配器修改为正常的RecyclerView.Adapter
并在其中构造了一个 AsyncDiffUtil (使用上面相同的 DiffUtil.Callback )。
该架构是:
- Activity -> Fragment(包含 RecyclerView)。
- Adapter
- 视图模型
- “假存储库”只包含一个
val source: MutableList<Thing> = mutableListOf()
Model
我创建了一个Thing
目的:data class Thing(val name: String = "", val age: Int = 0)
.
为了便于阅读,我添加了typealias Things = List<Thing>
(减少打字)。 ;)
存储库
它是假的,因为项目的创建方式如下:
private fun makeThings(total: Int = 20): List<Thing> {
val things: MutableList<Thing> = mutableListOf()
for (i in 1..total) {
things.add(Thing("Name: $i", age = i + 18))
}
return things
}
但“源”是(类型别名)的可变列表。
存储库可以做的另一件事是“模拟”随机项目的修改。我只是创建一个新的数据类实例,因为它显然都是不可变的数据类型(因为它们应该是)。请记住,这只是模拟可能来自 API 或数据库的真实更改。
fun modifyItemAt(pos: Int = 0) {
if (source.isEmpty() || source.size <= pos) return
val thing = source[pos]
val newAge = thing.age + 1
val newThing = Thing("Name: $newAge", newAge)
source.removeAt(pos)
source.add(pos, newThing)
}
视图模型
这里没什么特别的,它会说话并保存对ThingsRepository
,并公开 LiveData:
private val _state = MutableLiveData<ThingsState>(ThingsState.Empty)
val state: LiveData<ThingsState> = _state
而“状态”是:
sealed class ThingsState {
object Empty : ThingsState()
object Loading : ThingsState()
data class Loaded(val things: Things) : ThingsState()
}
viewModel 有两个公共方法(除了val state
):
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(ThingsState.Loaded(repository.fetchAllTheThings()))
}
}
fun modifyData(atPosition: Int) {
repository.modifyItemAt(atPosition)
fetchData()
}
没什么特别的,只是一种按位置修改随机项目的方法(记住这只是测试它的快速技巧)。
因此,FetchData 在 IO 中启动异步代码来“获取”(实际上,如果列表存在,则返回缓存的列表,只有第一次在存储库中“制作”数据时)。
修改数据更简单,在存储库上调用修改并获取数据以发布新值。
Adapter
很多样板文件......但正如所讨论的,它只是一个适配器:
class ThingAdapter(private val itemClickCallback: ThingClickCallback) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
The ThingClickCallback
只是:
interface ThingClickCallback {
fun onThingClicked(atPosition: Int)
}
该适配器现在有一个 AsyncDiffer...
private val differ = AsyncListDiffer(this, DiffUtilCallback())
this
在这种情况下是实际的适配器(不同需要)和DiffUtilCallback
只是一个DiffUtil.Callback
执行:
internal class DiffUtilCallback : DiffUtil.ItemCallback<Thing>() {
override fun areItemsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.age == newItem.age && oldItem.name == oldItem.name
}
这里没什么特别的。
适配器中唯一的特殊方法(除了 onCreateViewHolder 和 onBindViewHolder)是:
fun submitList(list: Things) {
differ.submitList(list)
}
override fun getItemCount(): Int = differ.currentList.size
private fun getItem(position: Int) = differ.currentList[position]
所以我们问differ
为我们做这些并公开公共方法submitList
模仿一个listAdapter#submitList(...)
,除非我们委托给不同的人。
因为您可能想知道,这是 ViewHolder:
internal class ViewHolder(itemView: View, private val callback: ThingClickCallback) :
RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.thingName)
private val age: TextView = itemView.findViewById(R.id.thingAge)
fun bind(data: Thing) {
title.text = data.name
age.text = data.age.toString()
itemView.setOnClickListener { callback.onThingClicked(adapterPosition) }
}
}
不要太严厉,我知道我直接通过了点击侦听器,我只有大约 1 小时来完成这一切,但没什么特别的,布局只是两个文本视图(年龄和姓名),我们将整行设置为可点击将位置传递给回调。这里也没什么特别的。
最后但并非最不重要的一点是,Fragment
.
Fragment
class ThingListFragment : Fragment() {
private lateinit var viewModel: ThingsViewModel
private var binding: ThingsListFragmentBinding? = null
private val adapter = ThingAdapter(object : ThingClickCallback {
override fun onThingClicked(atPosition: Int) {
viewModel.modifyData(atPosition)
}
})
...
它有3个成员变量。 ViewModel、Binding(我使用 ViewBinding 为什么不只是 gradle 中的 1 个行)和 Adapter(为了方便起见,它在 ctor 中采用 Click 侦听器)。
在此实现中,我只是使用“在位置 (X) 修改项目”来调用视图模型,其中 X = 在适配器中单击的项目的位置。 (我知道这可以更好地抽象,但这与这里无关)。
这个片段中只有两个其他实现的方法......
销毁时:
override fun onDestroy() {
super.onDestroy()
binding = null
}
(I wonder if Google will ever accept their mistake with Fragment's lifecycle that we still have to care for this).
无论如何,其他的也不足为奇,onCreateView
.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.things_list_fragment, container, false)
binding = ThingsListFragmentBinding.bind(root)
viewModel = ViewModelProvider(this).get(ThingsViewModel::class.java)
viewModel.state.observe(viewLifecycleOwner) { state ->
when (state) {
is ThingsState.Empty -> adapter.submitList(emptyList())
is ThingsState.Loaded -> adapter.submitList(state.things)
is ThingsState.Loading -> doNothing // Show Loading? :)
}
}
binding?.thingsRecyclerView?.adapter = adapter
viewModel.fetchData()
return root
}
绑定事物(根/绑定),获取viewModel,观察“状态”,在recyclerView中设置适配器,并调用viewModel开始获取数据。
就这样。
那么它是如何运作的呢?
应用程序启动,创建片段,订阅VMstate
LiveData,并触发数据的获取。
ViewModel 调用存储库,该存储库是空的(新的),因此调用 makeItems 列表现在包含项目并缓存在存储库的“源”列表中。 viewModel 异步接收此列表(在协程中)并发布 LiveData 状态。
片段接收状态并将其发布(提交)到适配器以最终显示一些内容。
当您“单击”某个项目时,ViewHolder(具有单击侦听器)会触发接收位置的片段的“回调”,然后将其传递到 Viewmodel 并这里的数据发生了变化在存储库中,它再次推送相同的列表,但对已修改的单击项目具有不同的引用。这会导致 ViewModel 向片段推送一个新的 LIveData 状态,该状态具有与之前相同的列表引用,该片段再次接收此状态,并执行 adapter.submitList(...)。
适配器异步计算此值并更新 UI。
它有效,如果你想玩得开心,我可以将所有这些放在 GitHub 中,但我的观点是,虽然对 AsyncDiffer 的担忧是有效的(并且可能是或曾经是真的),但这似乎不是我的(超级有限) ) 经验。
您以不同的方式使用它吗?
当我点击任何行时,更改将从存储库传播
UPDATE: 忘记包括doNothing
功能:
val doNothing: Unit
get() = Unit
我已经使用它有一段时间了,我通常使用它,因为它读起来比XXX -> {}
大部头书。 :)