DiffUtil 在嵌套的 recyclerview Kotlin 中不起作用
DiffUtil Not working in nested recyclerview Kotlin
我有两个回收站观点。我的视图直到我使用 notifyDataSetChanged
才更新。我要求类似类型的 issue, but this time I have Github Link。所以请看看并向我解释我做错了什么。谢谢
MainActivity.kt
package com.example.diffutilexample
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.diffutilexample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private var groupAdapter: GroupAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViewModel()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.fetchData()
binding.button.setOnClickListener {
viewModel.addData()
}
}
private fun setupViewModel() {
viewModel.groupListLiveData.observe(this) {
if (groupAdapter == null) {
groupAdapter = GroupAdapter()
binding.recyclerview.adapter = groupAdapter
}
groupAdapter?.submitList(viewModel.groupList?.toMutableList())
binding.recyclerview.post {
groupAdapter?.notifyDataSetChanged()
}
}
}
}
ActivityViewModel.kt
package com.example.diffutilexample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
var groupListLiveData: MutableLiveData<Boolean> = MutableLiveData()
var groupList: ArrayDeque<Group>? = null
set(value) {
field = value
groupListLiveData.postValue(true)
}
var value = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
groupList = groupByData(response.abc)
}
}
private fun groupByData(abc: List<Abc>?): ArrayDeque<Group> {
val result: ArrayDeque<Group> = groupList ?: ArrayDeque()
abc?.iterator()?.forEach { item ->
val key = GroupKey(item.qwe)
result.addFirst(Group(key, mutableListOf(item)))
}
return result
}
fun addData() {
groupList?.let { lastList ->
val qwe = Qwe("Vivek ${value++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val lastGroup = lastList[0]
lastGroup.list.add(item)
groupList = lastList
}
}
}
请在GithubLink中找到完整代码。我附在上面
我不完全确定,我承认我没有广泛研究您的代码,这不是解决方案,但这可能会为您指明解决问题的正确方向。
关于
的事情
groupAdapter?.submitList(viewModel.groupList?.toMutableList())
toMutableList()
是否确实复制了列表。但是列表中的每个对象都不是副本。如果您将内容添加到原始列表中的对象,就像您在 addData()
中所做的那样,它实际上也已经添加到适配器中的副本中。这就是为什么新的 submitList 不会将其识别为更改,因为它实际上与 submitList 之前相同。
据我所知,如果您提交的列表只包含不可变的对象,那么使用 DiffUtil 的效果最好,因此不会发生这样的错误。我之前 运行 遇到过类似的问题,解决方案也不简单。事实上,我不完全记得我当时是如何解决它的,但希望这能把你推向正确的方向。
我没有对此进行调试,但如果您消除对 MutableLists 和 var
s 的过度使用,并简化您的 LiveData,您可能会消除您的错误。至少,它会帮助你找到问题所在。
MutableLists 和 DiffUtil 不能一起玩!
例如,组的列表应该是只读列表:
data class Group(
val key: GroupKey,
val list: List<Abc?> = emptyList()
)
有一个 LiveData 只在其他 属性 可用时才报告,这让人费解。然后你要处理这里和观察者中的所有地方的可空性,因此很难判断某些代码何时会从空安全调用中跳过或不跳过。我会将您的 LiveData 更改为直接发布只读列表。您可以通过使用 emptyList()
来避免可空列表来简化代码。
您也可以避免使用 ArrayDeque 公开展示您的内部工作原理。而且您在不必要地延迟加载 ArrayDeque,这导致不必要地处理可空性。
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
private val _groupList = MutableLiveData<List<Group>>()
val groupList: LiveData<List<Group>> get() = _groupList
private val trackedGroups = ArrayDeque<Group>()
private var counter = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
addFetchedData(response.abc.orEmpty())
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
private fun addFetchedData(abcList: List<Abc>) {
for (item in abcList) {
val key = GroupKey(item.qwe)
trackedGroups.addFirst(Group(key, listOf(item)))
}
}
fun addData() {
if (trackedGroups.isEmpty())
return // Might want to create a default instead of doing nothing?
val qwe = Qwe("Vivek ${counter++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val group = trackedGroups[0]
trackedGroups[0] = group.copy(list = group.list + item)
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
在你的 Activity 中,由于你的 GroupAdapter 没有依赖项,你可以在调用站点实例化它以避免处理延迟加载它。并且你可以立即将它设置到onCreate()
中的RecyclerView。
因为ViewModel的改变,观察变得很简单
如果您在 setupViewModel()
中执行某些立即更新视图的操作,则会发生崩溃,因此您应该在调用 setContentView()
后移动它。
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private val groupAdapter = GroupAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).apply {
setContentView(root)
recyclerview.adapter = groupAdapter
button.setOnClickListener {
viewModel.addData()
}
}
setupViewModel()
viewModel.fetchData()
}
private fun setupViewModel() {
viewModel.groupList.observe(this) {
groupAdapter.submitList(it)
}
}
}
您在 GroupAdapter 中的 DiffUtil.ItemCallback.areItemsTheSame
不正确。你只应该检查它们是否代表相同的项目,而不是它们的内容是否相同,所以它不应该比较列表。
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem.key == newItem.key
}
而在 GroupViewHolder 中,每次反弹时,您都在为内部 RecyclerView 创建一个新的适配器。这完全违背了使用 RecyclerView 的目的。您应该只创建一次适配器。
我预测当视图被回收而不是仅仅被更新时,嵌套列表中的变化看起来会很奇怪,因为它会动画化之前视图中的变化,这可能来自不同的项目。所以我们应该跟踪旧的项目键并在新键不匹配时避免动画。我认为这可以在适配器中通过调用 notifyDataSetChanged()
更新列表内容后 submitList()
回调参数中 运行 完成,但我还没有测试过。
class GroupViewHolder(val binding: ItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
companion object {
//...
}
private val adapter = NestedGroupAdapter().also {
binding.nestedRecyclerview.adapter = it
}
private var previousKey: GroupKey? = null
fun bindItem(item: Group?) {
val skipAnimation = item?.key != previousKey
previousKey = item?.key
adapter.submitList(item?.list.orEmpty()) {
if (skipAnimation) adapter.notifyDataSetChanged()
}
}
}
旁注:您的适配器的 bindView
函数名称容易混淆。我只是将它们变成辅助构造函数,您可以将主构造函数设为私有。
class GroupViewHolder private constructor(private val binding: ItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
constructor(parent: ViewGroup) : this(
ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
//...
}
我有两个回收站观点。我的视图直到我使用 notifyDataSetChanged
才更新。我要求类似类型的 issue, but this time I have Github Link。所以请看看并向我解释我做错了什么。谢谢
MainActivity.kt
package com.example.diffutilexample
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.diffutilexample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private var groupAdapter: GroupAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViewModel()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.fetchData()
binding.button.setOnClickListener {
viewModel.addData()
}
}
private fun setupViewModel() {
viewModel.groupListLiveData.observe(this) {
if (groupAdapter == null) {
groupAdapter = GroupAdapter()
binding.recyclerview.adapter = groupAdapter
}
groupAdapter?.submitList(viewModel.groupList?.toMutableList())
binding.recyclerview.post {
groupAdapter?.notifyDataSetChanged()
}
}
}
}
ActivityViewModel.kt
package com.example.diffutilexample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
var groupListLiveData: MutableLiveData<Boolean> = MutableLiveData()
var groupList: ArrayDeque<Group>? = null
set(value) {
field = value
groupListLiveData.postValue(true)
}
var value = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
groupList = groupByData(response.abc)
}
}
private fun groupByData(abc: List<Abc>?): ArrayDeque<Group> {
val result: ArrayDeque<Group> = groupList ?: ArrayDeque()
abc?.iterator()?.forEach { item ->
val key = GroupKey(item.qwe)
result.addFirst(Group(key, mutableListOf(item)))
}
return result
}
fun addData() {
groupList?.let { lastList ->
val qwe = Qwe("Vivek ${value++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val lastGroup = lastList[0]
lastGroup.list.add(item)
groupList = lastList
}
}
}
请在GithubLink中找到完整代码。我附在上面
我不完全确定,我承认我没有广泛研究您的代码,这不是解决方案,但这可能会为您指明解决问题的正确方向。
关于
的事情groupAdapter?.submitList(viewModel.groupList?.toMutableList())
toMutableList()
是否确实复制了列表。但是列表中的每个对象都不是副本。如果您将内容添加到原始列表中的对象,就像您在 addData()
中所做的那样,它实际上也已经添加到适配器中的副本中。这就是为什么新的 submitList 不会将其识别为更改,因为它实际上与 submitList 之前相同。
据我所知,如果您提交的列表只包含不可变的对象,那么使用 DiffUtil 的效果最好,因此不会发生这样的错误。我之前 运行 遇到过类似的问题,解决方案也不简单。事实上,我不完全记得我当时是如何解决它的,但希望这能把你推向正确的方向。
我没有对此进行调试,但如果您消除对 MutableLists 和 var
s 的过度使用,并简化您的 LiveData,您可能会消除您的错误。至少,它会帮助你找到问题所在。
MutableLists 和 DiffUtil 不能一起玩!
例如,组的列表应该是只读列表:
data class Group(
val key: GroupKey,
val list: List<Abc?> = emptyList()
)
有一个 LiveData 只在其他 属性 可用时才报告,这让人费解。然后你要处理这里和观察者中的所有地方的可空性,因此很难判断某些代码何时会从空安全调用中跳过或不跳过。我会将您的 LiveData 更改为直接发布只读列表。您可以通过使用 emptyList()
来避免可空列表来简化代码。
您也可以避免使用 ArrayDeque 公开展示您的内部工作原理。而且您在不必要地延迟加载 ArrayDeque,这导致不必要地处理可空性。
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
private val _groupList = MutableLiveData<List<Group>>()
val groupList: LiveData<List<Group>> get() = _groupList
private val trackedGroups = ArrayDeque<Group>()
private var counter = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
addFetchedData(response.abc.orEmpty())
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
private fun addFetchedData(abcList: List<Abc>) {
for (item in abcList) {
val key = GroupKey(item.qwe)
trackedGroups.addFirst(Group(key, listOf(item)))
}
}
fun addData() {
if (trackedGroups.isEmpty())
return // Might want to create a default instead of doing nothing?
val qwe = Qwe("Vivek ${counter++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val group = trackedGroups[0]
trackedGroups[0] = group.copy(list = group.list + item)
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
在你的 Activity 中,由于你的 GroupAdapter 没有依赖项,你可以在调用站点实例化它以避免处理延迟加载它。并且你可以立即将它设置到onCreate()
中的RecyclerView。
因为ViewModel的改变,观察变得很简单
如果您在 setupViewModel()
中执行某些立即更新视图的操作,则会发生崩溃,因此您应该在调用 setContentView()
后移动它。
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private val groupAdapter = GroupAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).apply {
setContentView(root)
recyclerview.adapter = groupAdapter
button.setOnClickListener {
viewModel.addData()
}
}
setupViewModel()
viewModel.fetchData()
}
private fun setupViewModel() {
viewModel.groupList.observe(this) {
groupAdapter.submitList(it)
}
}
}
您在 GroupAdapter 中的 DiffUtil.ItemCallback.areItemsTheSame
不正确。你只应该检查它们是否代表相同的项目,而不是它们的内容是否相同,所以它不应该比较列表。
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem.key == newItem.key
}
而在 GroupViewHolder 中,每次反弹时,您都在为内部 RecyclerView 创建一个新的适配器。这完全违背了使用 RecyclerView 的目的。您应该只创建一次适配器。
我预测当视图被回收而不是仅仅被更新时,嵌套列表中的变化看起来会很奇怪,因为它会动画化之前视图中的变化,这可能来自不同的项目。所以我们应该跟踪旧的项目键并在新键不匹配时避免动画。我认为这可以在适配器中通过调用 notifyDataSetChanged()
更新列表内容后 submitList()
回调参数中 运行 完成,但我还没有测试过。
class GroupViewHolder(val binding: ItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
companion object {
//...
}
private val adapter = NestedGroupAdapter().also {
binding.nestedRecyclerview.adapter = it
}
private var previousKey: GroupKey? = null
fun bindItem(item: Group?) {
val skipAnimation = item?.key != previousKey
previousKey = item?.key
adapter.submitList(item?.list.orEmpty()) {
if (skipAnimation) adapter.notifyDataSetChanged()
}
}
}
旁注:您的适配器的 bindView
函数名称容易混淆。我只是将它们变成辅助构造函数,您可以将主构造函数设为私有。
class GroupViewHolder private constructor(private val binding: ItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
constructor(parent: ViewGroup) : this(
ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
//...
}