最近,我在我的应用程序中发现了奇怪的崩溃。我发现它们是由下面的ListAdapter-> DiffUtil引起的。契约(Contract)规定,仅当areContentsTheSame返回为相应项目的真时,才会调用areItemsTheSame回调。
问题是对于从未调用过areContentsTheSame的项目调用了areItemsTheSame

我正在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在后台线程上计算diff,而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 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
    }
}

问题解决后将更新答案

10-04 19:37