RecyclerView onClickListener 与 DiffUtil

RecyclerView onClickListener with DiffUtil

如何使用 DiffUtil 回调处理 RecyclerView 的 onClick?以及如何更改 recyclerview 中所选项目的背景颜色?我有两个 RecyclerViews 合二为一 Activity。当用户单击 RecyclerView A 中的项目时,RecyclerView B 中会发生一些事情。

这是class

import androidx.room.ColumnInfo

data class SkladTuple(
    @ColumnInfo(name = "sklad") val sklad: Int?,
    @ColumnInfo(name = "reg") val reg: Int?
)

这是适配器:

class SkladAdapter: ListAdapter<SkladTuple, SkladAdapter.PolozkaViewHolder>(DiffCallback())
{
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PolozkaViewHolder {
        val binding = SkladyItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return PolozkaViewHolder(binding)
    }

    override fun onBindViewHolder(holder: PolozkaViewHolder, position: Int) {
        val currentItem = getItem(position)
        holder.bind(currentItem)  

    }
    class PolozkaViewHolder(private val binding: SkladyItemBinding): RecyclerView.ViewHolder(binding.root){
        fun bind(polozkaSklad: SkladTuple){
            binding.apply {
                tvSklad.text = polozkaSklad.sklad.toString()
                tvRegal.text = polozkaSklad.reg.toString()

            }
        }
    }
    class DiffCallback: DiffUtil.ItemCallback<SkladTuple>(){
        override fun areItemsTheSame(oldItem: SkladTuple, newItem: SkladTuple) =
            oldItem.sklad == newItem.sklad

        override fun areContentsTheSame(oldItem: SkladTuple, newItem: SkladTuple) =
            oldItem == newItem
    }
}

这是Activity

@AndroidEntryPoint
class DokladActivity : AppCompatActivity() {
    private val skladViewModel: SkladViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityDokladBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.btVybratDoklad.setOnClickListener{
            openActivity(binding.root)
        }
        val skladAdapter = SkladAdapter()
        val dokladAdapter = DokladAdapter()
        binding.apply {
            recyclerViewSklady.apply {
                adapter = skladAdapter

                layoutManager = LinearLayoutManager(this@DokladActivity)
            }
            skladViewModel.skladyPolozky.observe(this@DokladActivity) {
                skladAdapter.submitList(it)
                Log.d("Doklad", skladAdapter.currentList.toString())
            }

            recyclerViewDoklady.apply {
                adapter = dokladAdapter
                layoutManager = LinearLayoutManager(this@DokladActivity)
            }
            skladViewModel.dokladyPolozky.observe(this@DokladActivity){
                dokladAdapter.submitList(it)
                Log.d("Doklad", dokladAdapter.currentList.toString())
            }
        }


    }

    fun openActivity(view: View){
        val intent = Intent(this,PolozkaActivity::class.java )
        startActivity(intent)
    }
}


这是 ViewModel

@HiltViewModel
class SkladViewModel @Inject constructor(
    repository: SybaseRepository
): ViewModel(){
    val skladyPolozky = repository.getAllSkladFromPolozka().asLiveData()
    val dokladyPolozky = repository.getAllHlavickyToDoklad().asLiveData()
}

这是布局 - 我有两个回收视图。当用户点击 recyclerview A 中的项目时,recyclerview B 中会发生一些事情

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline8"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.02" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline9"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.7" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline10"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.25" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline11"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.75" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline12"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.9" />

    <TableLayout
        android:stretchColumns="1,2"
        android:layout_margin="8dp"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toTopOf="@+id/guideline10"
        app:layout_constraintEnd_toStartOf="@+id/guideline9"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline8">

        <TableRow>
            <TextView
                android:text="SKLAD"
                android:textSize="16dp"
                android:textStyle="bold"
                android:padding="10dp"
                android:layout_gravity="center"
                android:layout_column="1"/>
            <TextView
                android:text="REGAL"
                android:textSize="16dp"
                android:textStyle="bold"
                android:padding="10dp"
                android:layout_gravity="center"
                android:layout_column="1"/>
        </TableRow>

        <androidx.recyclerview.widget.RecyclerView
            xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/recycler_view_sklady"
            android:scrollbars="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            tools:listitem="@layout/sklady_item"
            />

    </TableLayout>

<TableLayout
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_marginStart="8dp"
    android:layout_marginTop="8dp"
    android:layout_marginEnd="16dp"
    android:layout_marginBottom="8dp"
    android:stretchColumns="1,2,3,4,5"
    app:layout_constraintBottom_toTopOf="@+id/guideline11"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@+id/guideline10"
    >
    <TableRow>
        <TextView
            android:layout_column="1"
            android:layout_gravity="center"
            android:padding="10dp"
            android:text="U"
            android:textSize="16dp"
            android:textStyle="bold" />
        <TextView
            android:layout_column="1"
            android:layout_gravity="center"
            android:padding="10dp"
            android:text="DOKL"
            android:textSize="16dp"
            android:textStyle="bold" />
        <TextView
            android:layout_column="1"
            android:layout_gravity="center"
            android:padding="10dp"
            android:text="ODB"
            android:textSize="16dp"
            android:textStyle="bold" />
        <TextView
            android:layout_column="1"
            android:layout_gravity="center"
            android:padding="10dp"
            android:text="ORG"
            android:textSize="16dp"
            android:textStyle="bold" />
        <TextView
            android:layout_column="1"
            android:layout_gravity="center"
            android:padding="10dp"
            android:text="DATUM"
            android:textSize="16dp"
            android:textStyle="bold" />

    </TableRow>


    <androidx.recyclerview.widget.RecyclerView
        xmlns:app="http://schemas.android.com/apk/res-auto"

        android:id="@+id/recycler_view_doklady"
        android:scrollbars="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:listitem="@layout/doklady_item"/>
</TableLayout>

    <Button
        android:id="@+id/bt_back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:text="Zpět"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline12" />

    <Button
        android:id="@+id/bt_vybrat_doklad"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="24dp"
        android:text="Vybrat doklad"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline12" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="209dp"
        android:layout_height="50dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="16dp"
        android:text="TextView"
        app:layout_constraintBottom_toTopOf="@+id/guideline12"
        app:layout_constraintEnd_toStartOf="@+id/guideline9"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline11" />
</androidx.constraintlayout.widget.ConstraintLayout>

How is it possible to ...

你有一个 ViewModel,它公开了两个可观察的 liveData 和你的 RecyclerViews 需要的列表(两个适配器,两个列表)。

决定列表 A 的项目 1 被点击并因此列表 B 的项目 N 必须更改其背景的逻辑绝对 回收站视图的范围之外、适配器、Activity,甚至可能是 ViewModel。

最终,当发生数据突变时(您点击了项目 1),您会收到此事件,对其采取行动(又名:将其发送到 Viewmodel -> Repository ->转换数据 -> 使用变异数据发布新列表。

最终,您的 ViewModel 将从存储库接收新数据,并将更新您的 activity 正在观察的 LiveData,这将导致适配器提交新数据,您的DiffUtils 将启动,导致 Adapter 重新绑定修改后的视图。

更新

根据您链接的 question/answer 和您的评论,我认为这值得进一步评论。

  1. 此人说:“处理适配器上的点击或使用界面都不是一个好主意”,然后继续“处理”点击, 使用了一个匿名函数(所以它可能是一个用于测试目的的接口,但它却使它成为一个“高阶函数”)。

  2. 这和我描述的原理完全一样,只是接口更容易理解。

想想这样的责任:

ViewHolder:它对数据、回收器视图、适配器等一无所知。但它可以直接访问您的列表中的每个“row/column”。它拥有视图,并且可以根据它提供的数据来操纵它们,并告诉它与所需的视图一起“保存”。它不创建视图(它被告知)但它有它们。关于点击,所有这一切都可以做,要么公开一个视图(这样一个点击监听器可以由外部的人设置)或者简单地委托它。

适配器:除了它们的类型(如果使用)以及如何创建 viewHolders 和“绑定”它们之外,它对视图一无所知。所以它介于两者之间,但它确实可以访问提供给它的数据源(您提供的列表)并且它确实可以访问它创建的每个“视图持有者”;它或多或少知道屏幕上的内容,并且可以将位置映射到数据。

RecyclerView/Fragment/ViewModel:他们不知道适配器内部发生了什么(他们也不应该知道),但他们主要对点击(或事件)发生的时间感兴趣,因此他们大概可以对此做些什么。

基于所有这些,让我们看看 (与我的建议相比):

  1. Adapter 的构造函数修改为接收一个函数:

(private val onSelect: (YourDataType?) -> Unit)

我通常定义一个接口:

interface SomethingClickListener {
  fun onSomethingICaredForHappened(something: Something)
}

并将其传递给我的适配器,但思路是一样的;我更喜欢接口,因为它更容易使用具体类型的依赖注入(并且更容易测试),但我相信这只是个人偏好。

class YourAdapter(private val clickListener: SomethingClickListener)

  1. adapter内部也是一样的原理。您需要在您希望被点击的视图上设置实际的点击侦听器(或者如果您希望整个“行”都可点击,则设置整个 itemView)。

通过简单地使用点击侦听器的匿名实现并捕获发送到绑定方法的值(这很好),您可以(或想在其中做更多的事情)来获得链接的答案。因此,您可以使用另一个界面或那样做,本质上,想法是相同的,您 沿 传递点击事件。

binding.root.setOnClickListener {
    onSelect(yourDataType)
  }

所以它将 dataType 直接传递给 onSelect(这是提供给适配器的函数)。在这个阶段,其中唯一的逻辑是您附加与被点击的 viewBinding 关联的“数据”,这是 adapter/viewholder 可以 并且应该 供应。

在这个 onSelect 之后发生的事情是你的 fragment/activity 收到这个,然后你应该调用你的 ViewModel 说:

viewModel.thisThingWasClicked(theThingYouReceived)

你明白了......

我有一段时间没有更新它了(我想我是 3 年前在 Java 中最初写的,然后我天真地将它转换为 Kt)除了更新库并确保它仍然可以编译,但是您可以看到“双接口方法”(有点老派,现在可以通过 Kotlin 获得高阶函数),但仍然是相同的方法。

看看这个项目(特别是 adapter

注意它接受 ThingClickListener,但在内部使用 ToggleListener 在 ViewHolder 和 Adapter 之间进行对话。

最后,这可以简化为少用一个界面。

我希望这能澄清我的想法。 请注意,ViewHolder 和 Adapter 除了处理 Click 侦听器和绑定正确的数据之外没有任何责任。