首页 > 解决方案 > DiffUtil 违反 areContentTheSame 的合同 [下一个版本中的修复]

问题描述

最近我在我的应用程序中发现了奇怪的崩溃。我发现它们是由ListAdapter->DiffUtil下面引起的。合同说只有在相应项目返回trueareContentsTheSame时才会调用回调。对从未调用过的项目调用问题。areItemsTheSameareContentsTheSameareItemsTheSame

我正在对String项目进行测试,因此它不应该与我自己的回收站实施相关。如果是我的错(现在几乎没有逻辑)或DiffUtil工具中的错误,我真的很困惑

我创建了简单的仪器测试,但在提到的情况下失败了 - 有经验的人可以看看:

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 谢谢,等不及要删除所有解决方法!

标签: androidandroid-support-libraryandroid-recyclerviewlistadapterandroidx

解决方案


我得到了谷歌的回复,他们确认DiffUtil列表包含重复项(空值、相同对象等)时存在错误

我目前的解决方法是在执行之前自己检查“合同”,所以:

override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
    return compare items
}

override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
    //should be called only if areContentsTheSame == 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
    }
}

问题解决后将更新答案


推荐阅读