【问题标题】:RecyclerView list item view not updating (using DiffUtil.ItemCallback)RecyclerView 列表项视图未更新(使用 DiffUtil.ItemCallback)
【发布时间】:2020-11-19 06:48:11
【问题描述】:

已解决

RV - 回收站视图

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

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

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
    }
}

【问题讨论】:

  • 曾经尝试从 areContentsTheSame,areItemsTheSame 返回 false。我知道 DiffUtils 会失去它的用途,但它会再次刷新整个列表。
  • DiffUtil 需要提交新列表以与旧列表进行比较,您需要包含活动或片段加载 RV 适配器以获得正确答案。
  • 请将AlarmSongItem的源代码以及您收集它们的列表的位置发布给submitList
  • 请检查我的答案!

标签: android android-recyclerview android-databinding listadapter android-diffutils


【解决方案1】:

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

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

例子

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

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

【讨论】:

  • 嗨,Shawn,我的 DiffUtil 工作正常并向 recyclerView 提供更新的列表。我的问题是,视图仅在滚动时更新。请查看随附的视频
  • @AnkitGupta 我已经更新了我的答案,视频很有帮助..
【解决方案2】:

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

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

我最近遇到了同样的问题,我可以通过使用 onBindViewHolder(holder: AlarmSongsViewHolder, position: Int, payloads: MutableList&lt;Any&gt;) 轻松解决它

在您的 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()
    }
}

【讨论】:

    【解决方案3】:

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

    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)
    

    【讨论】:

    • stil 不起作用,在 diffResult.dispatchUpdatesTo(this) 之后,新列表没有绑定到持有者
    【解决方案4】:

    删除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
        }
    }
    

    【讨论】:

    • 请显示您点击时更新列表的代码并使用adapter.submitList()提交新列表
    • 实际更新只有在DiffUtil比较新旧Item(具体是最后一个状态和新状态)并相应更新UI时才会起作用。因此,您必须确保新的 AlarmSongItem 的 downloadState 与上一个不同。这样 DiffUtil 中的比较将更新 UI。
    • 问题是 diffUtil 正在更新新列表并将其提供给 Recyclerview,但视图没有更新,即使它有新数据。请查看提供的视频。
    • 我看过视频。它目前正在工作,因为当您向外滚动并向后滚动时,会使用更新的状态重新创建视图。 DiffUtil 也应该如此。我在我的一个应用程序中采用了类似的方法。 DiffUtil 能够立即做到这一点。只需确保新旧列表中的两个状态不同。
    【解决方案5】:

    您的问题:更新数据时 ViewHolder 未更新

    有三种方法可以触发 ViewHolder 重新加载内容:

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

    第一个选项似乎对你有用!

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

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

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

    【讨论】:

    • notifyDataSetChanged() 将刷新整个视图列表,这在任何情况下我都不想要。
    • @AnkitGupta 完全可以理解!这就是为什么我提到了调用adapter.notifyItemChanged(int position),你可以用它重新加载一个位置!调用notifyDataSetChanged()主要是为了完整性解释
    • 如果我必须使用 notifyitemchanged,我看不出使用 DiffUtils 的任何目的。
    • 如果您使用 DiffUtil,则不需要使用 notifyItemChanged,正如文档中所引用的那样。 " 它可用于计算 RecyclerView 适配器的更新。请参阅 ListAdapter 和 AsyncListDiffer,它们可以简化 DiffUtil 在后台线程上的使用。"
    • @Tobi 正如我已经提到的,DiffUtil 没有问题,recyclerView 及其视图有新数据,只是,即使有新数据,视图也不会更新。跨度>
    【解决方案6】:

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

    解决方案:

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

    DiffUtil 现在运行良好。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-23
      • 1970-01-01
      • 2022-07-28
      • 1970-01-01
      • 2012-04-02
      • 1970-01-01
      相关资源
      最近更新 更多