为什么UI有时无法通过数据绑定在recyclerview中更新?

时间:2019-09-15 19:38:53

标签: android kotlin android-recyclerview android-databinding

我在recyclerview中有一个5个元素的列表,像一个待办事项列表一样设置。每行复选框上都有一个侦听器,对于此最小可复制示例,每当您选中任何复选框时,它都会随机设置5个复选框的值。取消选中某个项目时,它应该以黑色文本显示,选中一个项目时,它应该以灰色文本和斜体显示。

当我选中一个框并重置值时,通常UI会按预期更新。但是,有时某个项目的布局错误,因此该复选框显示正确的值,但是文本样式错误。为什么此行为不一致,如何确保每次刷新UI?

这是整个MRE:

MainActivity.kt

".+_.+_p\\d+$"

ToDoItem.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.dalydays.android.mre_recyclerview_refresh_last_item.databinding.ActivityMainBinding
import kotlin.random.Random

class MainActivity : AppCompatActivity() {

    private lateinit var adapter: ToDoAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.lifecycleOwner = this

        binding.itemsList.layoutManager = LinearLayoutManager(this)

        val onCheckboxClickListener: (ToDoItem) -> Unit = { _ ->
            adapter.submitList(getSampleList())
        }

        adapter = ToDoAdapter(onCheckboxClickListener)

        binding.itemsList.adapter = adapter

        adapter.submitList(getSampleList())
    }

    private fun getSampleList(): List<ToDoItem> {
        val sampleList = mutableListOf<ToDoItem>()

        sampleList.add(ToDoItem(id=1, description = "first item", completed = Random.nextBoolean()))
        sampleList.add(ToDoItem(id=2, description = "second item", completed = Random.nextBoolean()))
        sampleList.add(ToDoItem(id=3, description = "third item", completed = Random.nextBoolean()))
        sampleList.add(ToDoItem(id=4, description = "fourth item", completed = Random.nextBoolean()))
        sampleList.add(ToDoItem(id=5, description = "fifth item", completed = Random.nextBoolean()))

        return sampleList
    }
}

ToDoAdapter.kt

data class ToDoItem(
    var id: Long? = null,
    var description: String,
    var completed: Boolean = false
)

activity_main.xml

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.dalydays.android.mre_recyclerview_refresh_last_item.databinding.ChecklistItemCheckedBinding
import com.dalydays.android.mre_recyclerview_refresh_last_item.databinding.ChecklistItemUncheckedBinding

const val ITEM_UNCHECKED = 0
const val ITEM_CHECKED = 1

class ToDoAdapter(private val onCheckboxClick: (ToDoItem) -> Unit): ListAdapter<ToDoItem, RecyclerView.ViewHolder>(ToDoItemDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ITEM_CHECKED -> ViewHolderChecked.from(parent)
            else -> ViewHolderUnchecked.from(parent)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val toDoItem = getItem(position)
        when (holder) {
            is ViewHolderChecked -> {
                holder.bind(toDoItem, onCheckboxClick)
            }
            is ViewHolderUnchecked -> {
                holder.bind(toDoItem, onCheckboxClick)
            }
        }
    }

    override fun getItemViewType(position: Int): Int {
        val toDoItem = getItem(position)
        return when (toDoItem.completed) {
            true -> ITEM_CHECKED
            else -> ITEM_UNCHECKED
        }
    }

    class ViewHolderChecked private constructor(private val binding: ChecklistItemCheckedBinding)
        : RecyclerView.ViewHolder(binding.root) {

        fun bind(toDoItem: ToDoItem, onCheckboxClick: (ToDoItem) -> Unit) {
            binding.todoItem = toDoItem
            binding.checkboxCompleted.setOnClickListener {
                onCheckboxClick(toDoItem)
            }
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): ViewHolderChecked {
                val layoutInflater = LayoutInflater.from(parent.context)
                return ViewHolderChecked(ChecklistItemCheckedBinding.inflate(layoutInflater, parent, false))
            }
        }
    }

    class ViewHolderUnchecked private constructor(private val binding: ChecklistItemUncheckedBinding)
        : RecyclerView.ViewHolder(binding.root) {

        fun bind(toDoItem: ToDoItem, onCheckboxClick: (ToDoItem) -> Unit) {
            binding.todoItem = toDoItem
            binding.checkboxCompleted.setOnClickListener {
                onCheckboxClick(toDoItem)
            }
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): ViewHolderUnchecked {
                val layoutInflater = LayoutInflater.from(parent.context)
                return ViewHolderUnchecked(ChecklistItemUncheckedBinding.inflate(layoutInflater, parent, false))
            }
        }
    }
}


class ToDoItemDiffCallback : DiffUtil.ItemCallback<ToDoItem>() {
    override fun areItemsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
        return oldItem.id == newItem.id
    }

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

checklist_item_checked.xml

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

        <data>

        </data>

        <RelativeLayout
            android:id="@+id/linearLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/items_list"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:paddingStart="8dp"
                    android:paddingEnd="8dp"
                    android:scrollbars="none" />

        </RelativeLayout>
</layout>

checklist_item_unchecked.xml

<?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="todoItem"
            type="com.dalydays.android.mre_recyclerview_refresh_last_item.ToDoItem" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <CheckBox
            android:id="@+id/checkbox_completed"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:checked="@{todoItem.completed}"
            android:textAppearance="?attr/textAppearanceListItem"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_description"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:ellipsize="end"
            android:text="@{todoItem.description}"
            android:textAppearance="?attr/textAppearanceListItem"
            android:textColor="#65000000"
            android:textStyle="italic"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/checkbox_completed"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Mow the lawn" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

2 个答案:

答案 0 :(得分:0)

修改这些方法

override fun areItemsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
    return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
    return ((oldItem.id == newItem.id) && (oldItem.description == newItem.description) && (oldItem.completed == newItem.completed) 
}

还覆盖方法getNewListSize()getOldListSize()

答案 1 :(得分:0)

也许不是“最佳”解决方案,但这是我想出的。我确定复选框动画和recyclerview刷新动画之间的时间安排可能存在问题。有时,根据recyclerview进行比较所花费的时间,recyclerview可以尝试在复选框完成动画制作之前或之后刷新。之前完成时,复选框动画将阻止recyclerview动画并使UI处于错误状态。否则,它似乎可以正常工作。

我决定手动运行adapter.notifyItemChanged(position),即使应该RecyclerView.ListAdapter自动处理它。根据差异计算的完成时间,它的动画效果仍然有些不一致,但这比使UI处于不良状态要好得多,并且比每次使用notifyDataSetChanged()刷新整个列表要好得多。

在MainActivity中,将复选框侦听器更改为此:

val onCheckboxClickListener: (ToDoItem, Int) -> Unit = { _, position ->
    adapter.submitList(getSampleList())
    adapter.notifyItemChanged(position)
}

在ToDoAdapter中,将类标题更改为此:

class ToDoAdapter(private val onCheckboxClick: (ToDoItem, Int) -> Unit): ListAdapter<ToDoItem, RecyclerView.ViewHolder>(ToDoItemDiffCallback()) {

并将两个bind()函数都更改为此:

fun bind(toDoItem: ToDoItem, onCheckboxClick: (ToDoItem, Int) -> Unit) {
    binding.todoItem = toDoItem
    binding.checkboxCompleted.setOnClickListener {
        onCheckboxClick(toDoItem, layoutPosition)
    }
    binding.executePendingBindings()
}