RecyclerView使用数据绑定和点击监听器

huangapple go评论51阅读模式
英文:

RecyclerView with databinding and onClick listener

问题

I'm starting using Kotlin (i'm a web dev) to maintain the mobile app of my current job. To practice my learning, I'm creating a basic app which is displaying a list of France departments (using a REST Api), and I need to allow the user to click on a list item to get more info on the selected item.
我正在开始使用Kotlin(我是一名Web开发人员)来维护我目前工作的移动应用。为了练习我的学习,我正在创建一个基本的应用程序,它显示了一个法国部门列表(使用REST Api),并且我需要允许用户单击列表项以获取所选项的更多信息。

I'm trying to build this with databinding, Koin as dependency injection, and Room as db layer.
我试图使用数据绑定、Koin作为依赖注入和Room作为数据库层来构建这个应用程序。

My issue is that I created a RecyclerView custom Adapter, and used the databinding to give it the datas. But now I want to implement the onClick behavior, which should launch another activity to display item details. My problem is: I don't know how to do this in a clean way.
我的问题是我创建了一个RecyclerView自定义适配器,并使用数据绑定来提供数据。但现在我想要实现点击行为,应该启动另一个活动来显示项目的详细信息。我的问题是:我不知道如何以一种干净的方式实现这一点。

I was thinking about creating a viewModel to link to my Adapter, but can't really find how to do it well. And even if I did, how to start another activity in a viewModel? (don't have access to the context and startActivity function). So I finally dropped that solution that doesn't seems to fit.
我曾考虑创建一个与我的适配器相关联的ViewModel,但实际上无法找到如何做到这一点的方法。而且,即使我这样做了,如何在ViewModel中启动另一个活动?(无法访问上下文和startActivity函数)。因此,我最终放弃了这个似乎不合适的解决方案。

So I'm currently thinking of passing directly from my adapter the onClick function, but can't find a way to bind this function in my xml file. Here is my files:
因此,我目前正在考虑直接从我的适配器中传递onClick函数,但找不到在我的xml文件中绑定此函数的方法。以下是我的文件:

MainActivity:
主活动:

class MainActivity : AppCompatActivity() {

    private val mViewModel: DepartmentsViewModel by viewModel()
    private lateinit var binding: ActivityMainBinding

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

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.toolbar.title = "Liste des départements"

        val adapter = DepartmentListAdaptater()
        binding.recyclerview.adapter = adapter
        binding.recyclerview.layoutManager = LinearLayoutManager(this)
        mViewModel.allDepartments.observe(this, Observer { data -> adapter.submitList(data) })
    }
}

RecyclerView.Adapter:
RecyclerView适配器:

class DepartmentListAdaptater : RecyclerView.Adapter<DepartmentListAdaptater.ViewHolder>() {
    private var dataSet: List<Department>? = null

    inner class ViewHolder(private val binding: DepartmentListRowBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(department: Department?) {
            binding.department = department
        }
    }

    fun submitList(list: List<Department>) {
        dataSet = list
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = DepartmentListRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)

        return ViewHolder(binding)
    }

    override fun getItemCount(): Int = dataSet?.size ?: 0

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(dataSet?.get(position))
    }
}

The XML View:
XML视图:

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

    <data>
        <variable name="department" type="com.navalex.francemap.data.entity.Department" />
    </data>

    <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="72dp">
        <LinearLayout
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:background="@drawable/list_item_bg"
                android:layout_alignParentEnd="true"
                android:layout_alignParentTop="true"
                android:layout_alignParentBottom="true"
                android:clickable="true"
                tools:ignore="UselessParent">

            <TextView
                    android:id="@+id/textView"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:paddingRight="16dp"
                    android:text="@{department.nom}"
                    android:paddingLeft="16dp"/>

        </LinearLayout>
    </RelativeLayout>
</layout>
英文:

I'm starting using Kotlin (i'm a web dev) to maintain the mobile app of my current job. To practice my learning, I'm creating a basic app which is displaying a list of France departments (using a REST Api), and I need to allow the user to click on a list item to get more info on the selected item.
I'm trying to build this with databinding, Koin as dependency injection, and Room as db layer.

My issue is that I created a RecyclerView custom Adapter, and used the databinding to give it the datas. But now I want to implement the onClick behaviour, which should launch another activity to display item details. My problem is: I don't know how to do this in a clean way.

I was thinking about creating a viewModel to link to my Adapter, but can't really find how to do it well. And even if I did, how to start another activity in a viewModel ? (don't have access to the context and startActivity function). So I finally dropped that solution that doesn't seems to fit.
So I'm currently thinking of passing directly from my adapter the onClick function, but can't find a way to bind this function in my xml file. Here is my files:

MainActivity:

class MainActivity : AppCompatActivity() {

    private val mViewModel: DepartmentsViewModel by viewModel()
    private lateinit var binding: ActivityMainBinding

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

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.toolbar.title = &quot;Liste des d&#233;partements&quot;

        val adapter = DepartmentListAdaptater()
        binding.recyclerview.adapter = adapter
        binding.recyclerview.layoutManager = LinearLayoutManager(this)
        mViewModel.allDepartments.observe(this, Observer { data -&gt; adapter.submitList(data) })
    }
}

RecyclerView.Adapter:

class DepartmentListAdaptater : RecyclerView.Adapter&lt;DepartmentListAdaptater.ViewHolder&gt;() {
    private var dataSet: List&lt;Department&gt;? = null

    inner class ViewHolder(private val binding: DepartmentListRowBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(department: Department?) {
            binding.department = department
        }
    }

    fun submitList(list: List&lt;Department&gt;) {
        dataSet = list
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = DepartmentListRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)

        return ViewHolder(binding)
    }

    override fun getItemCount(): Int = dataSet?.size ?: 0

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(dataSet?.get(position))
    }
}

The XML View:

&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;layout xmlns:tools=&quot;http://schemas.android.com/tools&quot;
        xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

    &lt;data&gt;
        &lt;variable name=&quot;department&quot; type=&quot;com.navalex.francemap.data.entity.Department&quot; /&gt;
    &lt;/data&gt;

    &lt;RelativeLayout
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;72dp&quot;&gt;
        &lt;LinearLayout
                android:orientation=&quot;vertical&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;wrap_content&quot;
                android:gravity=&quot;center_vertical&quot;
                android:background=&quot;@drawable/list_item_bg&quot;
                android:layout_alignParentEnd=&quot;true&quot;
                android:layout_alignParentTop=&quot;true&quot;
                android:layout_alignParentBottom=&quot;true&quot;
                android:clickable=&quot;true&quot;
                tools:ignore=&quot;UselessParent&quot;&gt;

            &lt;TextView
                    android:id=&quot;@+id/textView&quot;
                    android:layout_width=&quot;fill_parent&quot;
                    android:layout_height=&quot;wrap_content&quot;
                    android:paddingRight=&quot;16dp&quot;
                    android:text=&quot;@{department.nom}&quot;
                    android:paddingLeft=&quot;16dp&quot;/&gt;

        &lt;/LinearLayout&gt;
    &lt;/RelativeLayout&gt;
&lt;/layout&gt;

答案1

得分: 1

我不清楚数据绑定的具体情况,但一种典型的做法是让Activity处理应用程序导航等细节,然后让Adapter触发该逻辑。监听函数是一种简单的方法:

// 在您的Adapter中
var clickListener: ((YourData) -> ())? = null

// 在您的ViewHolder中(将其设置为内部类,以便可以访问Adapter的字段,如监听器对象和存储的数据)
init {
    clickableView.setOnClickListener {
        // 在这里传回任何数据,如果监听器需要知道被点击的是什么。我只是查找并传递当前显示的数据项
        clickListener?.invoke(
            adapterData[bindingAdapterPosition]
        )
    }
}

// 在设置适配器时,在您的Activity中
adapter.clickListener = { whateverData ->
    // 以响应点击执行您需要的操作
}

因此,Activity 本身正在定义关于点击发生时应采取的操作的逻辑 - 它基本上是连接应用程序的不同部分,因此Adapter 不需要关心除了获取数据、显示它并在发生特定交互时通知监听器以外的任何事情。该监听器代码(由Activity定义)可以导航到其他地方,或更新数据库,或将其传递给网络组件... 适配器不需要知道这些细节。

(以非 Kotlin 的方式执行此操作将是创建一个接口,然后让Activity实现它,并将自身作为监听器/回调对象传递,类似这样的方式)

英文:

I don't know about data binding specifically, but a typical way to do it is to let the Activity handle details like app navigation, and let the Adapter trigger that logic. A listener function is an easy way to do this:

// in your Adapter
var clickListener: ((YourData) -&gt; ())? = null

// in your ViewHolder (make it an inner class so it can access the Adapter&#39;s
// fields, like the listener object and the stored data)
init {
    clickableView.setOnClickListener {
        // pass back whatever data here, if the listener needs to know
        // what&#39;s been clicked. I&#39;m just doing a lookup and passing
        // the data item currently being displayed
        clickListener?.invoke(
            adapterData[bindingAdapterPosition]
        )
    }
}

// in your Activity, when setting up the adapter
adapter.clickListener = { whateverData -&gt;
    // do what you need to do in response to the click
}

So the Activity itself is defining that logic about actions that should be taken when a click happens - it's basically wiring up different parts of the app, so the Adapter doesn't need to be concerned with anything except taking data, displaying it, and informing a listener when specific interactions take place. That listener code (defined by the Activity) could navigate somewhere else, or update a database, or pass it to a networking component... the adapter doesn't need to know about that.

(The non-Kotlin way to do this would be to create an interface and have the Activity implement that, and pass itself as the listener/callback object, that kind of thing)

答案2

得分: 1

以下是您要翻译的内容:

"First I want to say that it's really impressive that you are a web developer and you already have a lot of knowledge about things like dependency injection and keep the state of the view on ViewModel, congrats. Now, let's talk about your problem... I'll start with some suggestions that will improve the code clarity and performance.

  1. For the Adapter implementation, always prefer to use ListAdapter, because this implementation have a more efficient way to compare the current list with the new list and update it. You can follow this example:
class MyAdapter: ListAdapter&lt;ItemModel, MyAdapter.MyViewHolder&gt;(DIFF_CALLBACK) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val binding = FragmentFirstBinding.inflate(LayoutInflater from(parent.context), parent, false)
        return MyViewHolder(binding)
    }

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

    class MyViewHolder(
        private val binding: FragmentFirstBinding
    ): RecyclerView.ViewHolder(binding.root) {

        fun bind(item: ItemModel) {
            // Here you can get the item values to put this values on your view
        }

    }

    companion object {
        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback&lt;ItemModel&gt;() {
            override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
                // need a unique identifier to have sure they are the same item. could be a comparison of ids. In this case, that is just a list of strings just compare like this below
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
                // compare the objects
                return oldItem == newItem
            }

        }
    }
}

In your fragment, you have a observer, that observe the value you want to sent to the adapter, right? When a update happen, you call the submitList sending the updated list and when the adapter receive this new list, the adapter will be responsible to update just the items that changed, because of your DIFF_CALLBACK implementation.

  1. About the onClick item, you can wait for a callback on your adapter. Doing this:
class MyAdapter(
    private val onItemClicked: (item: ItemModel) -&gt; Unit
): ListAdapter&lt;ItemModel, MyAdapter.MyViewHolder&gt;(DIFF_CALLBACK) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val binding = FragmentFirstBinding.inflate(LayoutInflater from(parent.context), parent, false)
        return MyViewHolder(binding, onItemClicked)
    }

    // ...

    class MyViewHolder(
        private val binding: FragmentFirstBinding,
        private val onItemClicked: (item: ItemModel) -&gt; Unit
    ): RecyclerView.ViewHolder(binding.root) {

        fun bind(item: ItemModel) {
            // ...
            // Here you set the callback to a listener
            binding.root.setOnClickListener {
                onItemClicked.invoke(item)
            }
        }

    }

    // ...
}

As you can see, we will receive the callback on the Adapter constructor, then we send to the ViewHolder by constructor too. And on the ViewHolder bind we set the callback to a click listener.

On you fragment, you will have something like this:

class MyFragment: Fragment()  {

    private lateinit adapter: MyAdapter
    
    private val onItemClicked: (itemModel: ItemModel) -&gt; Unit = { itemModel -&gt;
        // do something here when the item is clicked, like redirect to another activity
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        adapter = MyAdapter(onItemClicked)
    }
}

I hope it helps you. Please, let me know if you need something more. I really appreciate helping."

英文:

First I want to say that it's really impressive that you are a web developer and you already have a lot of knowledge about things like dependency injection and keep the state of the view on ViewModel, congrats. Now, let's talk about your problem... I'll start with some suggestions that will improve the code clarity and performance.

  1. For the Adapter implementation, always prefer to use ListAdapter, because this implementation have a more efficient way to compare the current list with the new list and update it. You can follow this example:
class MyAdapter: ListAdapter&lt;ItemModel, MyAdapter.MyViewHolder&gt;(DIFF_CALLBACK) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val binding = FragmentFirstBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return MyViewHolder(binding)
    }

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

    class MyViewHolder(
        private val binding: FragmentFirstBinding
    ): RecyclerView.ViewHolder(binding.root) {

        fun bind(item: ItemModel) {
            // Here you can get the item values to put this values on your view
        }

    }

    companion object {
        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback&lt;ItemModel&gt;() {
            override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
                // need a unique identifier to have sure they are the same item. could be a comparison of ids. In this case, that is just a list of strings just compare like this below
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
                // compare the objects
                return oldItem == newItem
            }

        }
    }
}

In your fragment, you have a observer, that observe the value you want to sent to the adapter, right? When a update happen, you call the submitList sending the updated list and when the adapter receive this new list, the adapter will be responsible to update just the items that changed, because of your DIFF_CALLBACK implementation.

  1. About the onClick item, you can wait for a callback on your adapter. Doing this:
class MyAdapter(
    private val onItemClicked: (item: ItemModel) -&gt; Unit
): ListAdapter&lt;ItemModel, MyAdapter.MyViewHolder&gt;(DIFF_CALLBACK) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val binding = FragmentFirstBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return MyViewHolder(binding, onItemClicked)
    }

    // ...

    class MyViewHolder(
        private val binding: FragmentFirstBinding,
        private val onItemClicked: (item: ItemModel) -&gt; Unit
    ): RecyclerView.ViewHolder(binding.root) {

        fun bind(item: ItemModel) {
            // ...
            // Here you set the callback to a listener
            binding.root.setOnClickListener {
                onItemClicked.invoke(item)
            }
        }

    }

    // ...
}

As you can see, we will receive the callback on the Adapter constructor, then we send to the ViewHolder by constructor too. And on the ViewHolder bind we set the callback to a click listener.

On you fragment, you will have something like this:

class MyFragment: Fragment()  {

    private lateinit var adapter: MyAdapter
    
    private val onItemClicked: (itemModel: ItemModel) -&gt; Unit = { itemModel -&gt;
        // do something here when the item is clicked, like redirect to another activity
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        adapter = MyAdapter(onItemClicked)
    }
}

I hope it helps you. Please, let me know if you need something more. I really appreciate helping.

huangapple
  • 本文由 发表于 2023年2月9日 00:44:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/75389002.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定