ListAdapter 未更新回收视图中的项

我正在使用新的支持库 ListAdapter

class ArtistsAdapter : ListAdapter<Artist, ArtistsAdapter.ViewHolder>(ArtistsDiff()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(parent.inflate(R.layout.item_artist))
}


override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}


class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(artist: Artist) {
itemView.artistDetails.text = artist.artistAlbums
.plus(" Albums")
.plus(" \u2022 ")
.plus(artist.artistTracks)
.plus(" Tracks")
itemView.artistName.text = artist.artistCover
itemView.artistCoverImage.loadURL(artist.artistCover)
}
}
}

我正在更新适配器

musicViewModel.getAllArtists().observe(this, Observer {
it?.let {
artistAdapter.submitList(it)
}
})

我的差异班

class ArtistsDiff : DiffUtil.ItemCallback<Artist>() {
override fun areItemsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem?.artistId == newItem?.artistId
}


override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}
}

当适配器第一次呈现所有项时调用 submitList,但是当使用更新的对象属性再次调用 submitList 时,它不会重新呈现已更改的视图。

当我滚动列表时,它重新呈现视图,然后调用 bindView()

另外,我注意到在提交列表之后调用 adapter.notifyDatasSetChanged()会显示具有更新值的视图,但是我不想调用 notifyDataSetChanged(),因为列表适配器内置了 diff utils

有人能帮我吗?

66902 次浏览

编辑: 我明白为什么会发生这种事,但这不是我的重点。我的观点是,它至少需要给出一个警告或调用 notifyDataSetChanged()函数。因为显然我调用 submitList(...)函数是有原因的。我敢肯定,人们花了几个小时试图找出问题所在,直到他们发现 submitList ()默默地忽略了调用。

这是因为 Google的奇怪逻辑。因此,如果将相同的列表传递给适配器,它甚至不会调用 DiffUtil

public void submitList(final List<T> newList) {
if (newList == mList) {
// nothing to do
return;
}
....
}

我真的不明白这个 ListAdapter的全部意义,如果它不能处理同一列表上的变化。如果你想改变你传递给 ListAdapter的列表上的项目,并且看到改变,那么你需要创建一个列表的深拷贝或者你需要使用你自己的 DiffUtill类的常规 RecyclerView

这个库假设您正在使用 Room 或任何其他 ORM,这些 ORM 在每次更新时都会提供一个新的异步列表,因此只要对其调用 submitList 就可以了,而对于草率的开发人员来说,如果调用同一个列表,就可以防止重复计算。

接受的答案是正确的,它提供了解释,但没有解决方案。

如果您没有使用任何这样的库,您可以做的是:

submitList(null);
submitList(myList);

另一个解决方案是覆盖 submitList (这不会导致快速闪烁) :

@Override
public void submitList(final List<Author> list) {
super.submitList(list != null ? new ArrayList<>(list) : null);
}

或者用科特林密码:

override fun submitList(list: List<CatItem>?) {
super.submitList(list?.let { ArrayList(it) })
}

有问题的逻辑,但完美的工作。 我首选的方法是第二种方法,因为它不会导致每一行都得到一个 onBind 调用。

根据 医生的官方数据:

每当你的 调用 submitList它提交一个 新的不同名单和显示。< br > < br > 这就是为什么 上一页上的 无论何时调用 submitList(已经提交的列表)、 它不计算 Diff不要通知适配器对数据集进行更改的原因。

我有一个类似的问题,但不正确的渲染是由 setHasFixedSize(true)android:layout_height="wrap_content"的组合造成的。第一次,适配器提供了一个空列表,所以高度从未被更新,是 0。总之,这解决了我的问题。其他人可能也有同样的问题,并且会认为这是适配器的问题。

今天我也偶然发现了这个“问题”。 在 Insa _ c 的回答RJFares 的解决方案的帮助下,我为自己设计了一个 Kotlin 延伸功能:

/**
* Update the [RecyclerView]'s [ListAdapter] with the provided list of items.
*
* Originally, [ListAdapter] will not update the view if the provided list is the same as
* currently loaded one. This is by design as otherwise the provided DiffUtil.ItemCallback<T>
* could never work - the [ListAdapter] must have the previous list if items to compare new
* ones to using provided diff callback.
* However, it's very convenient to call [ListAdapter.submitList] with the same list and expect
* the view to be updated. This extension function handles this case by making a copy of the
* list if the provided list is the same instance as currently loaded one.
*
* For more info see 'RJFares' and 'insa_c' answers on
* https://stackoverflow.com/questions/49726385/listadapter-not-updating-item-in-reyclerview
*/
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.updateList(list: List<T>?) {
// ListAdapter<>.submitList() contains (stripped):
//  if (newList == mList) {
//      // nothing to do
//      return;
//  }
this.submitList(if (list == this.currentList) list.toList() else list)
}

可在任何地方使用,例如:

viewModel.foundDevices.observe(this, Observer {
binding.recyclerViewDevices.adapter.updateList(it)
})

并且它只(并且总是)复制与当前加载的列表相同的列表。

如果在使用

recycler_view.setHasFixedSize(true)

你一定要看看这条评论: Https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531

它解决了我这边的问题。

(这里是评论的截图)

enter image description here

使用 Kotlin,你只需要像这样将列表转换为新的 可变列表,或者根据你的用法转换为其他类型的列表

.observe(this, Observer {
adapter.submitList(it?.toMutableList())
})

对我来说,这个问题出现,如果我使用 RecyclerView内的 ScrollViewnestedScrollingEnabled="false"和 RV 高度设置为 wrap_content
适配器正确更新并调用 bind 函数,但是没有显示项目—— RecyclerView被卡在了它的原始大小。

ScrollView改为 NestedScrollView修复了这个问题。

我需要修改我的柔化工具

override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {

实际返回内容是否是新的,而不仅仅是比较模型的 id。

在我的情况下,我忘记为 RecyclerView设置 LayoutManager。其效果与上面描述的相同。

对于任何人谁的情况与我相同,我留下我的解决方案,我不知道为什么它的工作,在这里。

我使用的解决方案是@Mina Samir,它将列表作为可变列表提交。

我的问题情景:

在一个片段中加载好友列表。

  1. ActivityMain 附加了 FragmentFriendList (观察好友数据库项目的实时数据) ,同时向服务器请求一个 http 请求以获取我所有的好友列表。

  2. 更新或插入来自 http 服务器的项。

  3. 每个更改都会引发 livedata 的 onChanged 回调。但是,当我第一次启动应用程序时(这意味着我的表上什么也没有) ,submitList 成功了,没有任何错误,但是屏幕上什么也没有出现。

  4. 然而,当我第二次启动应用程序时,数据被加载到屏幕上。

如上所述,解决方案是将列表作为 mutableList 提交。

使用@RJFares first response 成功更新列表,但不维护滚动状态。整个 RecyclerView从0开始。作为一种变通方法,我是这样做的:

   fun updateDataList(newList:List<String>){ //new list from DB or Network


val tempList = dataList.toMutableList() // dataList is the old list
tempList.addAll(newList)
listAdapter.submitList(tempList) // Recyclerview Adapter Instance
dataList = tempList


}

这样,我就能够维护 RecyclerView的滚动状态以及修改后的数据。

我也有过类似的问题。问题出在 Diff函数中,它没有充分比较项目。任何有此问题的人,请确保您的 Diff函数(以及扩展您的数据对象类)包含正确的比较定义-即比较可能在新项中更新的所有字段。例如在原文中

    override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}

这个函数(可能)不会执行它在标签上说的操作: 它不会比较两个项的内容——在 Artist类中覆盖了 equals()函数的 除非。在我的例子中,我没有这样做,而且由于我在实现它时的疏忽,areContentsTheSame的定义只检查了一个必要的字段。这是结构相等与引用相等,你可以找到更多关于它的 给你

未调用 ListAdapter.submitlist 的原因是对象 更新的地址在内存中仍然保持相同的地址。

当您使用 lets.setText 更新一个对象时,它会更改原始对象中的值。

这样,当检查 object.id = = object2.id 时,它将返回相同的值 因为两者在内存中都有对同一位置的引用。

解决方案是使用更新后的数据创建一个新对象,并将其插入到列表中。然后调用 submitList,它将正常工作

在同样的情况下,浪费了那么多时间去解决问题。

但是在我的情况下,问题是我忘记为我的回收视图指定一个 layoutManager: vRecyclerView.layoutManager = LinearLayoutManager(requireContext())

我希望没有人会重蹈我的覆辙。

最佳解决方案: 为了科特林

        var list :ArrayList<BaseModel> = ArrayList(adapter.currentList)
list.add(Item("Content"))
adapter.submitList(list) {
Log.e("ListAdaptor","List Updated Successfully")
}

我们不应该维护另一个基列表,因为 Adapter.currentList 将返回一个已经计算了 diff 的列表。

我们必须提供一个新的实例每次列表更新,因为区分工具 根据机器人文档 DiffUtil 是一个实用程序类,它计算两个列表之间的差异,并输出一个更新操作列表,该列表将第一个列表转换为第二个列表。 其中一个列表已经由 AsyncListDever 维护,它在后台线程上运行 defutil,另一个列表必须使用 Adaptor.submitList ()传递

它解决了我的问题。我认为最好的方法不是覆盖 submitList,而是添加一个新的函数来添加新的列表。

    fun updateList(list: MutableList<ScaleDispBlock>?) {
list?.let {
val newList = ArrayList<ScaleDispBlock>(list)
submitList(newList)
}
}

我有些奇怪的行为,我在 LiveDate 中使用 MutableList。

在 Kotlin 以下代码不起作用:

mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it)
})

但是,当我将它更改为 it.toList ()时,它就工作了

mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it.toList())
})

虽然“它”是同一份名单。

正如已经提到的,不能提交具有相同引用的 List,因为 ListAdapter 将看到列表位于相同的位置,因此不能使用 DiffUtil。

最简单的解决方案是制作列表的浅表副本。

submitList(ArrayList(list))

小心将 List 转换为 MutableList,因为这会为异常创建条件,并且很难找到 bug。

我使用的方法是覆盖 submitList()并创建一个传入列表的副本以及其中的每个项目:

override fun submitList(list: List<Item>?) {
val listCopy =
mutableListOf<Item>().apply {
list?.map {
add(Item(it.id, it.name, it.imageUrl))
}
}
super.submitList(listCopy)
}

我遇到了一个非常相似的问题。

数据列表更改后,我再次提交它,回收者视图没有按照我的要求显示。它显示了重复的项目。

我还没有找到根本原因,但我找到了一个变通方法,即再次将适配器设置为回收器视图。我想这会使回收站的观众忘记之前的记忆,并再次正确渲染。

userNftListFiltered = SOME_NEW_VALUE
binding.nftSendSearchList.adapter = searchNftAdapter //set adapter again
searchNftAdapter.submitList(userNftListFiltered)

enter image description here

这会有用的。 当你得到当前列表时,你是 < strong > 指向同一个位置的同一个列表

修改数组列表之后,必须让适配器知道应该更改哪个位置

下面的代码在我的情况下工作,希望它能有所帮助

private fun addItem() {
val index = myArrayList.size
val position = myArrayList.size+1
myArrayList.add(
index, MyArrayClass("1", "Item Name")
)
myAdapter.notifyItemInserted(position) // in case of insert


// in case of remove item
// val index = myArrayList.size-1
// myAdapter.notifyItemRemoved(index)
}

我也遇到了类似的问题,我的用例是我有一个 clickHandler 和项目将被选中/不选中(切换点击)。

从以上的答案中我尝试了大部分的方法,唯一有效的是

adapter.submitList(null)
adapter.submitList(modifiedList)

但问题是,每次我点击任何一个 clickHandler,整个列表都会被重新绘制,这是非常低效的。

我做了什么?

我做了一个实时数据,将存储最后一次点击项目,并观察到实时数据,我们可以告诉适配器,实时数据已经更新,如下所示

viewModel.lastClicked.observe(viewLifeCycleOwner, {
adapter.notifyItemChanged(it)
}

有一个 非常类似的问题,这一个,并决定 重新开始,甚至创建一个 GitHub项目来混淆。大多数解决方案对我来说都不太管用,甚至 toMutableList()的方法也不管用。在我的例子中,通过使用不可变的类和向 Adapter 提交不可变的 List,问题得到了解决。

只要在抬头之后调用 Adapter.notifyDataSetChanged ()即可

在我的例子中,我使用相同的对象(来自 adadptar)来更新 Room 数据库。 创建一个新对象来更新数据库,它会解决这个问题。

例如: 我正在做这个->

val playlist = adapter.getItem(position)
playlist.name = "new name"
updatePlaylistObjectInRoomDatabase(playlist)

上面的代码会在房间数据库之前改变适配器中的对象,所以 DiffUtil callback不会检测到任何改变。

现在这样做->

val playlist = adapter.getItem(position)
val newPlaylist = Playlist()
newPlaylist.id = playlist.id
newPlaylist.name = "new name"
updatePlaylistObjectInRoomDatabase(newPlaylist)

以上代码不会更改适配器列表中的任何内容,只会更改房间数据库中的数据。所以 submitList会有不同的值 DiffUtil callback可以检测到。

享受这些小事:)

这是官方 API 自然应该提供的东西,但由于它不是,这可以作为处理它的一种方式:

fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.clearItems() {
submitList(null)
submitList(emptyList())
}

适配器不能理解,你有一些更新,我不知道为什么! ? 我正在添加一些实体到列表广告,我希望收集他们在消费点。但是,什么都没发生。 作为一个对我有效的解决方案,你可以使用下面的脚本:

artistAdapter.submitList(it.toMutableList())

因为问题在于 ListAdapter内部,所以我想在 ListAdapter内部解决它。

感谢 Kotlin 扩展,我们可以这样写:

class MyItemAdapter() :
ListAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback) {


// ...


override fun submitList(list: List<Item>?) {
super.submitList(list?.toList())
}
}

它看起来确实像一个棘手的黑客。所以我也想做一个评论:

super.submitList(list?.toList()) // to make submitList work, new value MUST be a new list. https://stackoverflow.com/a/50031492/9735961

是的,谢谢你们,回收视图的开发者们。