ListAdapter 未更新 RecyclerView 中的项目
Posted
技术标签:
【中文标题】ListAdapter 未更新 RecyclerView 中的项目【英文标题】:ListAdapter not updating item in RecyclerView 【发布时间】:2018-09-18 11:09:31 【问题描述】:我正在使用新的支持库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
有人可以帮我吗?
【问题讨论】:
问题可能与ArtistsDiff
有关,因此与Artist
本身的实现有关。
是的,我也这么认为,但我似乎无法确定
您可以调试它或添加日志语句。您也可以将相关代码添加到问题中。
也检查一下这个问题,我以不同的方式解决了它***.com/questions/58232606/…
DiffUtil 使用 Eugene W. Myers 的差异算法来计算将一个列表转换为另一个列表的最小更新次数。简而言之,该算法在两个不同的列表上运行。 DiffUtil 由 AsyncListDiffer 使用,它在后台线程中运行算法并更新主线程上的回收器视图。 AsyncListDiffer 维护一个列表本身作为前一个列表容器,因此如果我们想要 Diffutils 的最佳性能,我们必须提供一个新的列表实例。
【参考方案1】:
编辑:我理解为什么会发生这种情况,这不是我的意思。我的观点是,它至少需要发出警告或调用notifyDataSetChanged()
函数。因为显然我调用submitList(...)
函数是有原因的。我很确定人们会在几个小时内试图弄清楚出了什么问题,直到他们发现 submitList() 默默地忽略了调用。
这是因为Google
s 奇怪的逻辑。因此,如果您将相同的列表传递给适配器,它甚至不会调用DiffUtil
。
public void submitList(final List<T> newList)
if (newList == mList)
// nothing to do
return;
....
我真的不明白这个ListAdapter
的全部意义,如果它不能处理同一个列表上的变化。如果您想更改传递给ListAdapter
的列表中的项目并查看更改,那么您需要创建列表的深层副本,或者您需要将常规RecyclerView
与您自己的DiffUtill
类一起使用。
【讨论】:
因为它需要以前的状态来执行差异。如果你覆盖之前的状态,它当然无法处理它。 O_o 是的,但是在那时,我打电话给submitList
是有原因的,对吧?它至少应该调用notifyDataSetChanged()
,而不是默默地忽略调用。我很确定人们会在几个小时内试图弄清楚出了什么问题,直到他们发现submitList()
默默地忽略了这个电话。
所以我回到 RecyclerView.Adapter<VH>
和 notifyDataSetChanged()
。现在的生活很好。浪费了好几个小时
@insa_c 你可以增加 3 个小时,这就是我浪费了多少时间试图理解为什么我的列表视图在某些边缘情况下没有更新......
notifyDataSetChanged()
很昂贵,并且完全违背了基于 DiffUtil 的实现的意义。仅使用新数据调用submitList
时可能会小心谨慎,但这实际上只是一个性能陷阱。【参考方案2】:
该库假定您正在使用 Room 或任何其他 ORM,它在每次更新时都会提供一个新的异步列表,因此只需在其上调用 submitList 即可,并且对于草率的开发人员来说,如果相同的列表,它会阻止计算两次被调用。
接受的答案是正确的,它提供了解释,但没有提供解决方案。
如果您不使用任何此类库,您可以做的是:
submitList(null);
submitList(myList);
另一种解决方案是像这样覆盖 submitList(不会导致快速闪烁):
@Override
public void submitList(final List<Author> list)
super.submitList(list != null ? new ArrayList<>(list) : null);
或者使用 Kotlin 代码:
override fun submitList(list: List<CatItem>?)
super.submitList(list?.let ArrayList(it) )
有问题的逻辑,但运行良好。 我首选的方法是第二种方法,因为它不会导致每一行都获得 onBind 调用。
【讨论】:
这是一个黑客。只需传递列表的副本。.submitList(new ArrayList(list))
我花了最后一个小时试图弄清楚我的逻辑有什么问题。好奇怪的逻辑。
@PaulWoitaschek 这不是黑客,这是使用 JAVA :) 它用于解决开发人员“沉睡”的库中的许多问题。您之所以选择这个而不是传递 .submitList(new ArrayList(list)) 是因为您可以在代码中的多个位置提交列表。您可能会忘记每次都创建一个新数组,这就是您覆盖的原因。
即使使用 Room,我也遇到了类似的问题。
显然这在使用新项目更新视图模型中的列表时有效,但是当我更新列表中项目的属性(布尔值 - isSelected)时,这仍然不起作用.. Idk 为什么但 DiffUtil 返回与我检查过的新旧项目相同。知道问题可能出现在哪里吗?【参考方案3】:
使用 Kotlin,您只需根据您的使用情况将列表转换为新的 MutableList 或其他类型的列表
.observe(this, Observer
adapter.submitList(it?.toMutableList())
)
【讨论】:
这很奇怪,但是将列表转换为 mutableList 对我有用。谢谢! 这到底为什么有效?它有效,但很好奇为什么会发生这种情况。 在我看来,ListAdapter 一定与您的列表引用无关,所以聪明吗?.toMutableList() 您向适配器提交一个新的实例列表。我希望对你来说足够清楚。 @March3April4 谢谢。根据您的评论,我猜想 ListAdapter 以 List.toMutableList()
创建了一个副本【参考方案4】:
我遇到了类似的问题,但不正确的渲染是由setHasFixedSize(true)
和android:layout_height="wrap_content"
的组合引起的。第一次,适配器提供了一个空列表,因此高度从未更新并且是0
。无论如何,这解决了我的问题。其他人可能有同样的问题,并认为这是适配器的问题。
【讨论】:
是的,将recycleview设置为wrap_content会更新列表,如果设置为match_parent则不会调用适配器【参考方案5】:如果您在使用过程中遇到一些问题
recycler_view.setHasFixedSize(true)
你一定要检查这个评论: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531
它解决了我这边的问题。
(这里是根据要求的评论截图)
【讨论】:
欢迎提供指向解决方案的链接,但请确保您的答案在没有它的情况下有用:在链接周围添加上下文,以便您的其他用户了解它是什么以及为什么存在它,然后引用在目标页面不可用的情况下,您链接到的页面的最相关部分。【参考方案6】:据官方docs:
每当您调用 submitList 时,它都会提交一个新列表进行比较 并显示。 这就是为什么每当您在上一个(已经提交的列表)上调用 submitList 时,它不会计算 Diff 并且不会通知适配器数据集发生变化。
【讨论】:
【参考方案7】:浪费了这么多时间来解决同一案例中的问题。
但在我的情况下,问题是我忘记为我的 recyclerView 指定 layoutManager:vRecyclerView.layoutManager = LinearLayoutManager(requireContext())
希望没有人会重蹈我的覆辙……
【讨论】:
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" 现在好多了:) 只是想为这个投票,因为我犯了同样的愚蠢错误。【参考方案8】:今天我也偶然发现了这个“问题”。 在insa_c's answer 和RJFares's solution 的帮助下,我为自己做了一个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://***.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)
)
如果列表与当前加载的列表相同,它只会(并且总是)复制列表。
【讨论】:
【参考方案9】:在我的例子中,我忘记为RecyclerView
设置LayoutManager
。其效果与上述相同。
【讨论】:
【参考方案10】:我有一些奇怪的行为。我在 LiveDate 中使用 MutableList。
在 kotlin 中,以下代码不起作用:
mViewModel.products.observe(viewLifecycleOwner,
mAdapter.submitList(it)
)
但是,当我将其更改为 it.toList() 时,它可以工作
mViewModel.products.observe(viewLifecycleOwner,
mAdapter.submitList(it.toList())
)
虽然,“它”是同一个列表。
【讨论】:
【参考方案11】:对我来说,如果我在 ScrollView
内部使用 RecyclerView
并使用 nestedScrollingEnabled="false"
并将 RV 高度设置为 wrap_content
,则会出现此问题。
适配器正确更新并调用了绑定函数,但未显示项目 - RecyclerView
卡在其原始大小。
将ScrollView
更改为NestedScrollView
解决了这个问题。
【讨论】:
【参考方案12】:我遇到了类似的问题。问题出在Diff
函数中,它没有充分比较这些项目。遇到此问题的任何人,请确保您的 Diff
函数(以及您的数据对象类的扩展)包含正确的比较定义 - 即比较可能在新项目中更新的所有字段。例如在原帖中
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean
return oldItem == newItem
此函数(可能)不会按照标签上的说明进行操作:它不会比较两个项目的内容 - 除非您已覆盖 Artist
中的 equals()
函数班级。就我而言,我没有,areContentsTheSame
的定义只检查了一个必要的字段,因为我在实施它时疏忽了。这是结构平等与引用平等,您可以找到更多关于它的信息here
【讨论】:
【参考方案13】:它解决了我的问题。我认为最好的方法是不要覆盖submitList
,而是添加一个新函数来添加新列表。
fun updateList(list: MutableList<ScaleDispBlock>?)
list?.let
val newList = ArrayList<ScaleDispBlock>(list)
submitList(newList)
【讨论】:
【参考方案14】:对于任何与我的情况相同的人,我将我的解决方案留在这里,我不知道它为什么会起作用。
对我有用的解决方案来自@Mina Samir,它将列表作为可变列表提交。
我的问题场景:
-在片段中加载好友列表。
ActivityMain 附加 FragmentFriendList(观察好友数据库项目的实时数据),同时向服务器请求 http 请求以获取我所有的好友列表。
从 http 服务器更新或插入项目。
每次更改都会触发 livedata 的 onChanged 回调。但是,当我第一次启动应用程序时,这意味着我的桌子上什么都没有,submitList 成功,没有任何类型的错误,但屏幕上什么也没有出现。
但是,当我第二次启动应用程序时,数据正在加载到屏幕上。
如上所述,解决方案是将列表作为 mutableList 提交。
【讨论】:
【参考方案15】:没有调用您的 ListAdapter .submitlist 的原因是因为该对象 您更新的内存中仍然保留相同的地址。
当您使用 .setText 更新对象时,它会更改原始对象中的值。
这样当您检查 object.id == object2.id 时,它会返回相同 因为两者都引用了内存中的相同位置。
解决方案是使用更新的数据创建一个新对象并将其插入到您的列表中。然后 submitList 将被调用,它会正常工作
【讨论】:
【参考方案16】:如前所述,您不能提交具有相同引用的列表,因为 ListAdapter 将看到列表位于同一位置,因此无法使用 DiffUtil。
最简单的解决方案是制作列表的浅表副本。
submitList(ArrayList(list))
小心将 List 转换为 MutableList,因为这会为异常创造条件并且难以发现错误。
【讨论】:
【参考方案17】:这将起作用.... 发生了什么当您获得当前列表时,您指向同一位置的同一列表
【讨论】:
创建一个新的可变列表的 Java 代码是什么?【参考方案18】:实际上你混淆了 ArtistsDiff 类中的覆盖函数
不是这个
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
使用这个
class ArtistsDiff : DiffUtil.ItemCallback<Artist>()
override fun areItemsTheSame(oldItem: Artist?, newItem: Artist?): Boolean
return oldItem == newItem // remove artistId
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean
return oldItem?.artistId == newItem?.artistId // add artistId
有关如何使用 ListAdapter 的更多信息,另请参阅 this post。
【讨论】:
【参考方案19】:我需要修改我的 DiffUtils
override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean
真正返回内容是否新,而不只是比较模型的id。
【讨论】:
【参考方案20】:使用@RJFares first answer成功更新列表,但不保持滚动状态。整个 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
的滚动状态以及修改后的数据。
【讨论】:
【参考方案21】:最优解: 对于科特林
var list :ArrayList<BaseModel> = ArrayList(adapter.currentList)
list.add(Item("Content"))
adapter.submitList(list)
Log.e("ListAdaptor","List Updated Successfully")
我们不应该维护另一个基本列表,因为 adapter.currentList 将返回一个已经计算 diff 的列表。
每次由于 DiffUtil 更新列表时,我们都必须提供一个新实例 根据android文档 DiffUtil 是一个实用程序类,它计算两个列表之间的差异并输出一个更新操作列表,将第一个列表转换为第二个列表。 一个列表已经由 AsyncListDiffer 维护,它在后台线程上运行 diffutil,另一个必须使用 adapter.submitList() 传递
【讨论】:
【参考方案22】:对我有用的方法是覆盖 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)
【讨论】:
【参考方案23】:我遇到了一个非常相似的问题。
数据列表更改后,我再次提交,回收站视图没有按我的意愿显示。它显示重复的项目。
我还没有找到根本原因,但是我找到了一个解决方法,即再次将适配器设置为回收器视图。我想这会让recycler viewer忘记之前的内存并再次正确渲染。
userNftListFiltered = SOME_NEW_VALUE
binding.nftSendSearchList.adapter = searchNftAdapter //set adapter again
searchNftAdapter.submitList(userNftListFiltered)
【讨论】:
【参考方案24】:一旦你修改了数组列表,你必须让适配器知道应该改变哪个位置
下面的代码在我的情况下工作希望它可以帮助
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)
【讨论】:
【参考方案25】:我也遇到了类似的问题,我的用例是我有一个 clickHandler 并且项目将被选中/未选中(点击时切换)。
我尝试了上述答案中的大部分方法,唯一有效的是
adapter.submitList(null)
adapter.submitList(modifiedList)
但问题是每次我点击任何 clickHandler 时,都会重新绘制整个列表,这是非常低效的。
我做了什么?
我制作了一个实时数据,将存储最后点击的项目并观察实时数据,我们可以告诉适配器实时数据已经更新,如下所示
viewModel.lastClicked.observe(viewLifeCycleOwner,
adapter.notifyItemChanged(it)
【讨论】:
【参考方案26】:有一个非常类似的问题,到这个问题,并决定open a new thread 甚至创建一个GitHub 项目来搞乱。大多数解决方案都不太适合我,甚至 toMutableList()
方式也不行。在我的例子中,问题是通过使用不可变类并将不可变的List
s 提交给适配器来解决的。
【讨论】:
以上是关于ListAdapter 未更新 RecyclerView 中的项目的主要内容,如果未能解决你的问题,请参考以下文章
使用 ListAdapter 更新 recyclerview 中的一行
从android eclipse中的片段刷新或更新listadapter
ListAdapter Diff 不会在同一个列表实例上分派更新,但也不会在与 LiveData 不同的列表上分派更新
带有 DiffUtil (ListAdapter) 的 RecyclerView 会阻塞 UI 线程