最近我在我的应用程序中发现了奇怪的崩溃。我发现它们是由下面的 ListAdapter
-> DiffUtil
引起的。契约(Contract)规定,仅当 areItemsTheSame
为相应项目返回 true 时,才会调用 areContentsTheSame
回调。
问题是为从未调用过 areItemsTheSame
的项目调用 areContentsTheSame
。
我正在 String
项目上测试它,所以它不应该与我自己的回收器实现相关。我真的很困惑,如果这是我的错(现在几乎没有逻辑)或 DiffUtil
工具中的错误
我已经创建了简单的 Instrumented Test,但在上述情况下失败了 - 更有经验的人可以看一下吗:
package com.example.diffutilbug
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import junit.framework.Assert.assertTrue
import kotlinx.coroutines.*
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.BlockJUnit4ClassRunner
@RunWith(BlockJUnit4ClassRunner::class)
internal class ExampleUnitTest {
@Test
fun testDiffUtil4() {
val handler = CoroutineExceptionHandler { _, exception ->
throw exception
}
// adapter compare items :
// areItemsTheSame -> compare length of String
// areContentsTheSame -> compare content with ==
val adapter = StringAdapterJunit(handler)
runBlocking {
adapter.submitList(
mutableListOf<String>(
"1",//1,
"22",//2,
"333",//3,
"4444",//4,
"55555",//5,
"666666",//6,
"7777777",//7,
"88888888",//8,
"999999999",//9,
"55555",//5,
"1010101010",//10,
"1010109999",//10,
"55555",//5,
"1313131313",//10,
"1414141414",//10,
"55555",//5,
"1313131313",//10,
"1414141414",//10,
"55555"//5
)
)
delay(40)
adapter.submitList(
mutableListOf<String>(
"55555",//5,
"1010101010",//10,
"1010109999",//10,
"55555",//11,
"1313131313",//10,
"1414141414",//10,
"11111111111"//11
)
)
delay(500)
}
}
}
// Stub Adapter for Strings that uses DiffUtil underneath.
// logs all callbacks to logcat
class StringAdapterJunit(val handler: CoroutineExceptionHandler) : ListAdapter<String, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
Log.e("DiffUtilTest", "areItemsTheSame comparing $oldItem with $newItem = ${oldItem.length == newItem.length}")
return oldItem.length == newItem.length
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
//should be called only if areContentsTheSame == true
Log.e(
"DiffUtilTest",
"areContentsTheSame error = ${oldItem.length != newItem.length} comparing $oldItem with $newItem"
)
runBlocking {
GlobalScope.launch(handler + Dispatchers.Main) {
assertTrue("areContentsTheSame can be called only if areItemsTheSame return true" , areItemsTheSame(oldItem, newItem))
}.join()
}
return oldItem == newItem
}
override fun getChangePayload(oldItem: String, newItem: String): Any? {
//should be called only if areItemsTheSame = true and areContentsTheSame = false
Log.e(
"DiffUtilTest",
"getChangePayload error = ${oldItem.length == newItem.length && oldItem == newItem} $oldItem with $newItem"
)
return null
}
}) {
// stub implementation on adapter - never used
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = object : RecyclerView.ViewHolder(View(null)) {}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {}
override fun getItemViewType(position: Int): Int = getItem(position).length
}
及其所需的 gradle 依赖项:
dependencies {
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
//coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
}
请注意需要添加
android.useAndroidX=true
android.enableJetifier=true
在你的gradle.properties
添加了协程和异常处理程序,因为 DiffUtil
在后台线程上计算差异,而 JUnit
仅在主线程上处理断言
============================================= ======
在下一个 alpha 中修复: 将在 alpha 3 中发布 - PR 以照顾 https://android-review.googlesource.com/c/platform/frameworks/support/+/1253271 谢谢,迫不及待地想删除所有解决方法!
最佳答案
我收到了 google 的回复,他们确认当列表包含重复项(空值、相同对象等)时 DiffUtil
中存在错误
我目前的解决方法是在执行前自己检查“契约(Contract)”,因此:
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return compare items
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
//should be called only if areItemsTheSame == true
return areItemsTheSame(oldItem, newItem) && compare items contents
}
override fun getChangePayload(oldItem: String, newItem: String): Any? {
//should be called only if areItemsTheSame = true and areContentsTheSame = false
if(areItemsTheSame(oldItem, newItem) && !areContentsTheSame(oldItem, newItem)) {
return compute changePayload
} else {
return null
}
}
问题解决后会更新答案
关于android - DiffUtil 违反了 areContentTheSame 的契约(Contract) [下一版本将修复],我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54364098/