android - DiffUtil 违反了 areContentTheSame 的契约(Contract) [下一版本将修复]

标签 android android-support-library android-recyclerview listadapter androidx

最近我在我的应用程序中发现了奇怪的崩溃。我发现它们是由下面的 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/

相关文章:

java - 使用 AES/CBC/NOPADDING 在 Node 中加密并使用相同算法在 JAVA 中解密,会产生一些垃圾,例如 e�J�,�d�|*�5har��

android - 在新版本的 Android 中使用支持类

android - 如何在针对 API 级别 14 的应用程序中使用 SwipeRefreshLayout?

android - lateinit 属性绑定(bind)尚未初始化

c# - 如何在 SpannableString Android 中设置不透明度/alpha

android - 如何判断 'Mobile Network Data' 是启用还是禁用(即使通过 WiFi 连接)?

android - 如何在 recyclerview 项目中使用自定义 View ?

android - 更新recyclerview中的值

android - 启用 Android 支持库调试标志的最佳方式

android - 如何使用支持库 23 修复缩放的 FloatingActionButton 上的阴影