ListAdapter Diff 不会在同一列表实例上调度更新,但也不会在与 LiveData 不同的列表上调度更新

2023-12-24

这是一个已知问题,如果新列表仅具有修改的项目但具有相同的实例,则 ListAdapter(实际上是其实现中的 AsyncListDiffer)不会更新列表。如果您在内部使用相同的对象,则更新也不适用于新实例列表。

为了使所有这些工作正常进行,您必须创建整个列表和其中对象的硬拷贝。 实现这一目标的最简单方法:

items.toMutableList().map { it.copy() }

但我面临着一个相当奇怪的问题。我的 ViewModel 中有一个解析函数,它最终将 items.toMutableList().map { it.copy() } 发布到 LiveData 并在片段中获取观察结果。即使有硬拷贝,DiffUtil 也不起作用。如果我将硬拷贝移动到片段内,那么它就可以工作。

为了更容易做到这一点,如果我这样做:

在视图模型中:

   [ ... ] parse stuff here

items.toMutableList().map { it.copy() }
restaurants.postValue(items)

在片段中:

 restaurants.observe(viewLifecycleOwner, Observer { items ->
     adapter.submitList(items)

……那么,就不行了。但如果我这样做:

在视图模型中:

   [ ... ] parse stuff here

restaurants.postValue(items)

在片段中:

 restaurants.observe(viewLifecycleOwner, Observer { items ->
     adapter.submitList(items.toMutableList().map { it.copy() })

...然后就可以了。

有人能解释为什么这不起作用吗?

与此同时,我在 Google 问题跟踪器上提出了一个问题,因为也许他们会修复 AsyncListDiffer 不更新相同实例列表或项目的问题。它违背了新适配器的目的。异步列表差异应该始终接受相同的实例列表或项目,并使用用户在适配器中自定义的差异逻辑进行完全更新。


我使用以下方法制作了一个快速样本DiffUtil.Callback and ListAdapter<T, K>(所以我在适配器上调用了submitList(...)),并且没有任何问题。

然后我将适配器修改为正常的RecyclerView.Adapter并在其中构造了一个 AsyncDiffUtil (使用上面相同的 DiffUtil.Callback )。

该架构是:

  1. Activity -> Fragment(包含 RecyclerView)。
  2. Adapter
  3. 视图模型
  4. “假存储库”只包含一个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开始获取数据。

就这样。

那么它是如何运作的呢?

应用程序启动,创建片段,订阅VMstateLiveData,并触发数据的获取。 ViewModel 调用存储库,该存储库是空的(新的),因此调用 makeItems 列表现在包含项目并缓存在存储库的“源”列表中。 viewModel 异步接收此列表(在协程中)并发布 LiveData 状态。 片段接收状态并将其发布(提交)到适配器以最终显示一些内容。

当您“单击”某个项目时,ViewHolder(具有单击侦听器)会触发接收位置的片段的“回调”,然后将其传递到 Viewmodel 并这里的数据发生了变化在存储库中,它再次推送相同的列表,但对已修改的单击项目具有不同的引用。这会导致 ViewModel 向片段推送一个新的 LIveData 状态,该状态具有与之前相同的列表引用,该片段再次接收此状态,并执行 adapter.submitList(...)。

适配器异步计算此值并更新 UI。

它有效,如果你想玩得开心,我可以将所有这些放在 GitHub 中,但我的观点是,虽然对 AsyncDiffer 的担忧是有效的(并且可能是或曾经是真的),但这似乎不是我的(超级有限) ) 经验。

您以不同的方式使用它吗?

当我点击任何行时,更改将从存储库传播

UPDATE: 忘记包括doNothing功能:


val doNothing: Unit
    get() = Unit

我已经使用它有一段时间了,我通常使用它,因为它读起来比XXX -> {}大部头书。 :)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ListAdapter Diff 不会在同一列表实例上调度更新,但也不会在与 LiveData 不同的列表上调度更新 的相关文章

  • 任务“:app:dexDebug”执行失败

    我目前正在处理我的项目 我决定将我的 Android Studio 更新到新版本 但在我导入项目后 它显示如下错误 Information Gradle tasks app assembleDebug app preBuild UP TO
  • 合并两个位图图像(并排)

    任何人都可以帮助将两个位图图像合并为单个位图 在android中 并排 谢谢 尤瓦拉吉 您可以使用Canvas 查看这篇文章 http www jondev net articles Combining 2 Images in Androi
  • ImageView 中的全尺寸图像

    我正在尝试在 ImageView 中绘制图像 但我希望它不缩放 并根据需要使用滚动条 我怎样才能做到这一点 现在我只有一个可绘制集作为 XML 中 ImageView 的 android src 这会自动缩放图像以适应屏幕宽度 我读到这可能
  • 为什么 Kotlin 数据类可以在 Gson 的不可空字段中包含空值?

    在 Kotlin 中你可以创建一个data class data class CountriesResponse val count Int val countries List
  • 更改 AChartEngine 中的图例大小

    我想专门更改饼图的图例大小输出 我已经尝试了所有可以找到的 AChartEngine 方法 但没有一个只能更改图例文本大小 我必须重写 onDraw 函数吗 如果是这样 怎么办 要设置图例高度 请使用 renderer setLegendH
  • MI设备中即使应用程序被杀死,如何运行后台服务

    您好 我正在使用 alaram 管理器运行后台服务 它工作正常 但对于某些 mi 设备 后台服务无法工作 我使用了服务 但它无法工作 如何在 mi 中运行我的后台服务 MI UI有自己的安全选项 所以你需要的不仅仅是上面提到的粘性服务 你需
  • layout.xml 的用途是什么?

    人们为什么使用layout xml在他们的resources like
  • 如何在照片删除后刷新 Android 的 MediaStore

    问题 如何使媒体存储刷新其已删除文件的条目 从外部存储中删除代码中的照片后 我仍然在图库中看到已删除照片的插槽 空白照片 画廊似乎反映了媒体存储 并且在媒体存储中找到了已删除的照片 直到手机重新启动或通常 直到重新扫描媒体为止 尝试扫描已删
  • 6:需要显示BuyFlow UI

    There is a problem when i am click on payWithGoogle Button I am implementing Google Pay in my Android Application and wh
  • Flutter / FireStore:如何在 Flutter 中显示 Firestore 中的图像?

    我想将我在应用程序中使用的一些图像放入 Firestore 并从那里显示它们 而不是将它们作为资产捆绑在我的应用程序中 为了做到这一点 我想出了以下解决方案 对于我想要显示图像的项目 我创建了一个 Firebase 文档 其中有一个字段存储
  • invalidateOptionsMenu 在片段中不起作用

    显示或隐藏项目ActionBar根据文本中是否有文本EditText or not 所以 我做了以下事情 public class NounSearch extends android app Fragment EditText seach
  • 当参数具有默认值时,为什么无法使用导航组件将参数传递给片段?

    我正在使用导航组件 但我不明白为什么如果定义了参数 则将参数传递给下面的方法时会出现错误 我正在使用 SafeArgs 只有当我为此参数定义默认值时才会出现此错误 有人可以解释一下为什么会发生这种情况以及如何解决它吗 这是导航图的部分代码
  • Facebook LoginActivity 未正确显示

    我有一个使用 Facebook 登录的应用程序 我有 FacebookSDK 并且使用 com facebook LoginActivity 问题是 在 10 英寸平板电脑上 当显示软键盘时 活动无法正确显示 我使用的是 Samsung G
  • onTouchEvent()中如何区分移动和点击?

    在我的应用程序中 我需要处理移动和单击事件 一次点击是由一个 ACTION DOWN 操作 多个 ACTION MOVE 操作和一个 ACTION UP 操作组成的序列 理论上 如果您收到 ACTION DOWN 事件 然后收到 ACTIO
  • Android:如何使视图增长以填充可用空间?

    这看起来很简单 但我不知道该怎么做 我有一个带有 EditText 和两个 ImageButtons 的水平布局 我希望 ImageButtons 具有固定大小 并且 EditText 占据布局中的剩余空间 如何才能做到这一点
  • ormlite 将日期读取为 'yyyy-MM-dd'

    我需要读取给我的 sqlite 数据库 因此我无法更改表中的日期格式 yyyy MM dd 当我尝试使用 ormlite 为我生成对象时 使用以下注释 DatabaseField columnName REVISION DATE dataT
  • 基于BluetoothChat示例通过蓝牙套接字发送文件

    大家好 根据我之前问的一个问题 我已经能够将文件转换为其他字节数组 以便使用以下写入方法 public void sendFile Log d TAG sending data InputStream inputStream null Ur
  • Android应用程序kill事件捕获

    我想在我的应用程序被终止时执行一些操作 可以使用哪种方法来实现此目的 我正在开发 Android 5 0 这个问题的关键在于 您必须了解您的申请是否可以收到任何 当您的应用程序在任何情况下被终止时的额外回调 下面的答案是由德文连线 http
  • Amazon IAP 不会调用 onPurchaseResponse

    我有一个 Android 应用程序 它使用 IAP 我正在发送PurchasingManager initiateGetUserIdRequest 并得到用户识别成功 in onGetUserIdResponse 得到回复后Purchasi
  • 修改 ADW Android 启动器?

    我想更改和修改开源 ADW 启动器 启动器可在此处获取 https github com AnderWeb android packages apps Launcher https github com AnderWeb android p

随机推荐