RecyclerView 列表项视图未更新(使用 DiffUtil.ItemCallback)

RecyclerView list item view not updating (using DiffUtil.ItemCallback)

已解决

RV - 回收站观点

我在警报对话框中有一个 RV。 RV 的适配器使用 DiffUtil.ItemCallback 扩展了 ListAdapter。适配器列表使用倒计时每 500 毫秒更新一次(检查列表项是否已下载)。

问题是,列表已更新并使用新数据提交给适配器,但列表项视图未根据提供的新数据进行更新,如下所示。我正在使用 data/view 绑定来更新列表项视图。

滚动时 RV 有时会更新项目视图。

PS:RV 是 NestedScrollView 的子项

This is how it is working right now

适配器代码

class AlarmSongsAdapter(
    private val onItemClicked: (AlarmSongItem) -> Unit,
    private val startDownloading: (String) -> Unit,
    private val insertDownloadEntityInDB: (DownloadEntity) -> Unit
) : ListAdapter<AlarmSongItem, AlarmSongsAdapter.AlarmSongsViewHolder>(DiffUtilCallback) {

object DiffUtilCallback : DiffUtil.ItemCallback<AlarmSongItem>() {
    override fun areItemsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean {
        return oldItem == newItem
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlarmSongsViewHolder {
    return AlarmSongsViewHolder(AlarmsSongListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), onItemClicked, startDownloading, insertDownloadEntityInDB)
}

override fun onBindViewHolder(holder: AlarmSongsViewHolder, position: Int) {
    holder.bind(getItem(position))
}

class AlarmSongsViewHolder(
    private val binding: AlarmsSongListItemBinding,
    private val onItemClicked: (AlarmSongItem) -> Unit,
    private val startDownloading: (String) -> Unit,
    private val insertDownloadEntityInDB: (DownloadEntity) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
    fun bind(alarmSongItem: AlarmSongItem) {
        binding.alarmSongItem = alarmSongItem
        binding.executePendingBindings()
    }

    init {
        binding.downloadButton.setOnClickListener {
            val alarmSongItem = binding.alarmSongItem!!
            when(alarmSongItem.downloadState){
                Download.STATE_STOPPED -> {
                    startDownloading(alarmSongItem.audioFile)
                    val storageInfo = StorageUtils.currentStorageTypeAndPath(binding.root.context)
                    insertDownloadEntityInDB(alarmSongItem.toDownloadEntity(storageInfo))
                }
                else -> {}
            }
        }

        binding.root.setOnClickListener {
            onItemClicked(binding.alarmSongItem!!)
        }
    }
}
}

列表项查看代码

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
    <variable
        name="alarmSongItem"
        type="com.baja.app.domain.models.AlarmSongItem" />
</data>

<com.google.android.material.card.MaterialCardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    app:cardElevation="5dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp">

        <androidx.cardview.widget.CardView
            android:id="@+id/song_item_thumbnail_container"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:cardBackgroundColor="@android:color/transparent"
            app:cardCornerRadius="6dp"
            app:cardElevation="0dp">

            <ImageView
                android:id="@+id/song_item_thumbnail"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_centerVertical="true"
                android:scaleType="centerCrop"
                app:srcCompat="@drawable/bg_default_light"
                tools:ignore="ContentDescription"
                app:thumbnailFromUri="@{alarmSongItem.thumbnail}" />

        </androidx.cardview.widget.CardView>

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="60dp"
            android:id="@+id/download_progress_container"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true">

            <ImageView
                android:id="@+id/download_bg"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:scaleType="centerCrop"
                app:srcCompat="?bg_default_circular"
                tools:ignore="ContentDescription"
                android:layout_centerInParent="true" />

            <com.google.android.material.button.MaterialButton
                android:id="@+id/download_button"
                style="@style/AppTheme.OutlinedButton.Icon"
                android:layout_width="32dp"
                android:layout_height="32dp"
                app:cornerRadius="32dp"
                app:icon="@drawable/ic_download"
                app:iconTint="@android:color/white"
                changeIcon="@{alarmSongItem.downloadState}"
                android:layout_centerInParent="true" />

            <com.google.android.material.progressindicator.ProgressIndicator
                android:id="@+id/download_progress_bar"
                style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
                android:layout_width="33dp"
                android:layout_height="33dp"
                app:circularRadius="17dp"
                app:indicatorColor="?attr/progressIndicatorColor"
                app:indicatorWidth="1dp"
                showProgressBar="@{alarmSongItem.downloadState}"
                android:layout_centerInParent="true"
                android:visibility="gone" />

        </RelativeLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_marginStart="20dp"
            android:layout_toEndOf="@id/song_item_thumbnail_container"
            android:orientation="vertical"
            android:weightSum="2"
            android:layout_toStartOf="@id/download_progress_container"
            android:layout_marginEnd="8dp">

            <TextView
                android:id="@+id/song_item_name"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:ellipsize="end"
                android:gravity="bottom"
                android:maxLines="1"
                android:textSize="16sp"
                android:textStyle="bold"
                tools:text="Sa re ga ma pa"
                android:text="@{alarmSongItem.title}" />


            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/song_item_artist"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:layout_marginEnd="4dp"
                    android:ellipsize="end"
                    android:gravity="center_vertical"
                    android:maxWidth="150dp"
                    android:maxLines="1"
                    android:textSize="14sp"
                    tools:text="Sidharth Arun"
                    android:text="@{alarmSongItem.artist}" />

                <View
                    android:layout_width="5dp"
                    android:layout_height="5dp"
                    android:layout_gravity="center_vertical"
                    android:background="@drawable/dot" />

                <TextView
                    android:id="@+id/song_item_duration"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:layout_marginStart="4dp"
                    android:ellipsize="end"
                    android:gravity="center_vertical"
                    android:maxLines="1"
                    tools:text="10:12"
                    app:formatDuration="@{alarmSongItem.duration}" />

            </LinearLayout>
        </LinearLayout>
    </RelativeLayout>

</com.google.android.material.card.MaterialCardView>

绑定适配器函数

@BindingAdapter("thumbnailFromUri")
fun thumbnailFromUri(view: ImageView, uri: String) {
    Glide.with(view).load(uri).placeholder(R.drawable.bg_default_light).error(R.drawable.bg_default_light).into(view)
}

@BindingAdapter("changeIcon")
fun changeIconBasedOnDownloadState(view: MaterialButton, state: Int) {
    when (state) {
        Download.STATE_COMPLETED -> view.setIconResource(R.drawable.ic_check)
        else -> view.setIconResource(R.drawable.ic_download)
    }
}

@BindingAdapter("showProgressBar")
fun showProgressbarBasedOnState(view: ProgressIndicator, state: Int) {
    when (state) {
        Download.STATE_QUEUED,
        Download.STATE_RESTARTING,
        Download.STATE_DOWNLOADING -> view.visibility = View.VISIBLE
        else -> view.visibility = View.GONE
    }
}

删除changeIconBasedOnDownloadState并将代码放入bind()。 假设 AlarmSongItem.downloadState 在新的 list 中有一个不同的 value,这就是你需要做的。

将您的绑定适配器代码移动到bind()

fun bind(alarmSongItem: AlarmSongItem) {
    binding.alarmSongItem = alarmSongItem
    binding.executePendingBindings()

     when (alarmSongItem.downloadState) {
        Download.STATE_QUEUED,
        Download.STATE_RESTARTING,
        Download.STATE_DOWNLOADING -> view.visibility = View.VISIBLE
        else -> view.visibility = View.GONE
    }
}

您的问题:更新数据时ViewHolder没有更新

可以通过三种方式触发 ViewHolder 重新加载内容:

  1. 你将它滚动出屏幕并返回 -> 因为它是一个 recyclerView 它将重用 ViewHolder 用于另一个项目,当你向上滚动时,它会重新加载第一个项目 -> 更新了 img
  2. 当您通知适配器某项已更改时
  3. 使用 DiffUtils 并通过调用 diffResult.dispatchUpdatesTo(adapter)
  4. 重新加载 ViewHolder

第一个选项似乎适合您!

第二个:
如果您调用 adapter.notifyDataSetChanged() 它将重新加载所有 ViewHolders。
如果您调用 adapter.notifyItemChanged(int position) 它将重新加载特定项目位置。

要了解问题的根源,您可能想尝试一下,看看问题是否更深层次。

第三个:
请显示计算 DiffUtil 结果的代码。

您需要向您的适配器添加一个方法,该方法将在更新列表时调用

class AlarmSongsAdapter(alarmSongItems: List<AlarmSongItem>) : RecyclerView.Adapter<AlarmSongsAdapter.ViewHolder>() {

    private val mAlarmSongItems = mutableListOf<AlarmSongItem>()

    init {
        mAlarmSongItems.addAll(alarmSongItems)
    }

    fun swap(alarmSongItems: List<AlarmSongItem>) {
            val diffCallback = DiffUtilCallback(this.mAlarmSongItems, alarmSongItems)
            val diffResult = DiffUtil.calculateDiff(diffCallback)
    
            this.mAlarmSongItems.clear()
            this.mAlarmSongItems.addAll(alarmSongItems)
            diffResult.dispatchUpdatesTo(this)
        }
}

mAlarmSongItems 是您传递给适配器的初始列表(抱歉,我没有复制您所有的变量,只是需要显示差异有意义的那个)

您的回电,

class DiffUtilCallback(
    private val oldList: List<AlarmSongItem>,
    private val newList: List<AlarmSongItem>
) : DiffUtil.Callback() {

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
    }

}

现在,无论您在哪里收到更新的倒数计时器值,在哪里初始化适配器,您都可以

alarmAdapter.swap(updatedAlarmValues)

该视频对查明问题非常有帮助。

您的 Diffutils“areContentsTheSame()”正在检查项目而不是项目的单个 属性。下载文件后,您需要让“areContentsTheSame()”检查下载 属性 以判断特定 属性.

是否有变化

例子

class MyDiffCallback : DiffUtil.ItemCallback<Dev>() {
    ... 

    override fun areContentsTheSame(oldItem: Dev, newItem: Dev): Boolean {
        return oldItem.downloadStatus == newItem.download.status && 
        oldItem == newItem
    }
}

The problem is, the list is updated and submitted to the adapter with the new data and but the list item view is not updating based on new data provided as shown below. I'm using data/view binding for updating the list item view.

发生这种情况是因为您向 submitList() 提交了相同的列表。您可以查看此 了解更多信息

我最近遇到了同样的问题,使用 onBindViewHolder(holder: AlarmSongsViewHolder, position: Int, payloads: MutableList<Any>)

可以很容易地解决它

在你的 DiffUtilCallback 中:

const val BUNDLE_TIME = "bundle_time"
object DiffUtilCallback : DiffUtil.ItemCallback<AlarmSongItem>() {
    override fun areItemsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean = false

    // This will be called every time you submit a list (so every 500ms)
    override fun getChangePayload(oldItem: AlarmSongItem, newItem: AlarmSongItem): Any {
        val diffBundle = Bundle()

        // pass the data you want to update
        diffBundle.putLong(BUNDLE_TIME, newItem.time)

        return diffBundle
    }
}

然后在您的适配器覆盖中: 注意最后有payloads:MutableList

override fun onBindViewHolder(holder: AlarmSongsViewHolder, position: Int, payloads: MutableList<Any>) {
    if(payloads.isEmpty()) {
        // if empty it's a new item that appears on the screen
        super.onBindViewHolder(holder, position, payloads)
        return
    }
    payloads.forEach { when(it) {
        is Bundle -> {
            val time = it.getLong(BUNDLE_TIME)
            holder.binding.alarmSongItem.time.text = time.toString()
        }
    }}
}

如果您不想公开 binding

,您甚至可以在 ViewHolder 中创建一个函数来传递要更新的数据
class AlarmSongsViewHolder(
    private val binding: AlarmsSongListItemBinding,
    private val onItemClicked: (AlarmSongItem) -> Unit,
    private val startDownloading: (String) -> Unit,
    private val insertDownloadEntityInDB: (DownloadEntity) -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(alarmSongItem: AlarmSongItem) {
        binding.alarmSongItem = alarmSongItem
        binding.executePendingBindings()
    }

    fun updateMyItem(time: Long) {
        binding.alarmSongItem.time.text = time.toString()
    }
}

我已经解决了这个问题(以我的方式)。感谢所有的答案,它们真的很有帮助,但不是在我的用例中。

解法:

为了使其准确工作,我在 rv_list_item.xml 中为下载详细信息(如状态、百分比等)创建了另一个变量,并传递了相应的下载。

DiffUtil 现在运行良好。