使用Kotlin正确处理RecyclerView的点击量
许多开发者倾向于以错误的方式处理RecyclerView 上的点击。在本教程中,我们将重点介绍处理RecyclerView 上的点击的最合适的方式。
前提条件
要跟上本教程,读者应该。
- 知道如何在
Android Studio中创建一个项目。 - 对
Kotlin有一定的了解。 - 使用过基本的
RecyclerView,以显示来自API的数据(我们将从REST API获取数据)。 - 知道Retrofit是什么以及如何使用它。
- 对Jetpack库有一定的了解,如
ViewModel和LiveData。 - 熟悉
ViewBinding。
目标
在本教程结束时,读者应该拥有。
- 概述了什么是
RecyclerView。 - 了解了
ListAdapter和DiffUtil。 - 有能力处理
RecyclerView中的点击。
简介
RecyclerView 是一个显示数据项列表的伟大部件。RecyclerView 类支持显示数据的集合。
什么是ListAdapter?
根据官方文档。ListAdapter是一个RecyclerView.Adapter基类,用于在RecyclerView中显示List数据,包括在后台线程中计算Lists之间的差异。
当使用LiveData<List> ,ListAdapter提供了一个简单的方法来提供数据给适配器。当有新数据时,你可以使用submitList(POJO)。它可以处理项目的添加和删除,而不需要重新绘制整个视图。它还能将这些变化变成动画。
什么是DiffUtil?
根据官方文档。DiffUtil是一个实用类,它计算两个列表之间的差异,并输出一个更新操作的列表,将第一个列表转换为第二个列表。
这个类消除了调用notifyDataSetChanged() 方法的需要。我们覆盖了它的两个方法;areItemsTheSame(oldItem: Pojo, newItem: Pojo) 和areContentsTheSame(oldItem: Pojo, newItem: Pojo) 。
第一个方法检查这两个对象是否相同(例如,基于id)。而第二个检查两个对象之间的数据是否相同。DiffUtil 使得ListAdapter 可以改变列表中的项目。
我们将从api.imgflip.com/get_memesAPI中获取memes。然后在一个RecyclerView 中显示它们,并添加一个点击监听器来处理对每一行的点击。
第1步:创建项目
在这一步,我们将创建一个新的项目,你可以参考下面的图片进行指导。

第2步:添加依赖性
创建项目后,我们将添加一些依赖项到我们的应用级构建Gradle。
Retrofit 将帮助我们对API进行网络调用。 将发挥将JSON字符串转换为Java对象的作用。GsonConverter
// Retrofit and Gson Converter for Networking
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Glide for Image Loading
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
// ViewModels and LiveData
def lifecycle_version = "2.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
请确保在应用级构建中也启用ViewBinding 。gradlegradle buildFeatures{ viewBinding true } 。
ViewBinding 生成一个Java类,代替代码中的findViewById。
第3步:XML布局
在这一步,我们将为回收器行和主布局创建布局。
首先,我们将设计Recycler行,它将有一个ImageView ,显示备忘录的照片,也有一个TextView ,显示备忘录的名称。接下来,我们将设计主要活动。它将有一个RecyclerView ,一个ProgressBar ,和一个TextView ,当我们的API调用失败时将显示一个错误。
回收器行布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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="wrap_content"
android:layout_margin="5dp"
app:cardElevation="5dp"
app:cardCornerRadius="15dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="200dp"
android:scaleType="centerInside"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/memeName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/imageView"
app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toBottomOf="@+id/imageView"
tools:text="Drake Hotline Bling" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
主布局
<?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"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/recycler_row" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/recyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textViewFailed"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/recyclerView"
tools:text="TextView" />
</androidx.constraintlayout.widget.ConstraintLayout>
第4步:模型类
在这一步中,我们将提出我们的模型类,我们只对备忘录的名称(name )和备忘录的图片(url )感兴趣。现在,我们将忽略其他的属性。
package com.kanyideveloper.recyclerviewitemclicksdemo
import com.google.gson.annotations.SerializedName
data class Memes(
@SerializedName("data")
val `data`: Data?,
@SerializedName("success")
val success: Boolean?
)
data class Data(
@SerializedName("memes")
val memes: List<Meme?>?
)
data class Meme(
@SerializedName("box_count")
val boxCount: Int?,
@SerializedName("height")
val height: Int?,
@SerializedName("id")
val id: String?,
@SerializedName("name")
val name: String?,
@SerializedName("url")
val url: String?,
@SerializedName("width")
val width: Int?
)
第5步:Api服务类
在这一步,我们将设计我们的ApiService,以便与Retrofit进行API调用。我们的基本URL将是https://api.imgflip.com/ ,端点将是get_memes 。
package com.kanyideveloper.recyclerviewitemclicksdemo
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
interface ApiService {
@GET("get_memes")
fun getPhotos(): Call<Memes>
}
object MemesApi {
private const val BASE_URL = "https://api.imgflip.com/"
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}
第6步:RecyclerView适配器类
在这一步,我们将创建我们的适配器类,它将完成所有的点击操作。
首先,我们创建一个名为OnClickListener 的类,在其构造函数中以一个meme项目作为参数,接收一个lambda。这个类包含一个名为onClick 的匹配函数。它将被设置为lambda的参数。所有这些创建了一种命名的lambda。
class OnClickListener(val clickListener: (meme: Meme) -> Unit) {
fun onClick(meme: Meme) = clickListener(meme)
}
然后我们在MemesAdapter 的构造函数上添加了一个onClickListener 属性。
class MemesAdapter(private val onClickListener: OnClickListener) :
ListAdapter<Meme, MemesAdapter.MyViewHolder>(MyDiffUtil)
最后,在这个类中,我们将通过在onBindViewHolder 内调用我们的onClickListener 来完成。
holder.itemView.setOnClickListener {
onClickListener.onClick(meme)
}
下面是整个类的实现。
package com.kanyideveloper.recyclerviewitemclicksdemo
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.bumptech.glide.Glide
import com.kanyideveloper.recyclerviewitemclicksdemo.databinding.RecyclerRowBinding
class MemesAdapter(private val onClickListener: OnClickListener) :
ListAdapter<Meme, MemesAdapter.MyViewHolder>(MyDiffUtil) {
companion object MyDiffUtil : DiffUtil.ItemCallback<Meme>() {
override fun areItemsTheSame(oldItem: Meme, newItem: Meme): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Meme, newItem: Meme): Boolean {
return oldItem.id == newItem.id
}
}
inner class MyViewHolder(private val binding: RecyclerRowBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(meme: Meme?) {
Glide.with(binding.imageView)
.load(meme?.url)
.into(binding.imageView)
binding.memeName.text = meme?.name
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
RecyclerRowBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val meme = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(meme)
}
holder.bind(meme)
}
class OnClickListener(val clickListener: (meme: Meme) -> Unit) {
fun onClick(meme: Meme) = clickListener(meme)
}
}
第7步:ViewModel类
接下来,我们将创建一个ViewModel类,它将包含能在配置变化中存活的代码,即屏幕旋转。特别是,它有一个函数,将执行对API的网络请求。
package com.kanyideveloper.recyclerviewitemclicksdemo
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MainViewModel : ViewModel() {
private val _reponse = MutableLiveData<Memes>()
val response: LiveData<Memes>
get() = _response
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean>
get() = _loading
private val _failed = MutableLiveData<String>()
val failed: LiveData<String>
get() = _failed
init {
_loading.value = true
getApiResponse()
}
private fun getApiResponse() {
MemesApi.apiService.getPhotos().enqueue(object : Callback<Memes> {
override fun onResponse(call: Call<Memes>, response: Response<Memes>) {
_response.value = response.body()
_loading.value = false
}
override fun onFailure(call: Call<Memes>, t: Throwable) {
_loading.value = false
_failed.value = t.localizedMessage
}
})
}
}
第8步:总结MainActivity
在这最后一步,我们将通过向MemesAdapter添加OnClickListener 对象来实例化这个适配器。这将从适配器中返回一个meme。
现在,我们可以尝试对点击的meme的名字进行Toast。在其他情况下,可能需要移动到另一个活动或片段,并显示被点击项目的细节。
package com.kanyideveloper.recyclerviewitemclicksdemo
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.kanyideveloper.recyclerviewitemclicksdemo.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }
private lateinit var adapter: MemesAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
adapter = MemesAdapter(MemesAdapter.OnClickListener { photo ->
Toast.makeText(applicationContext, "${photo.name}", Toast.LENGTH_SHORT).show() })
viewModel.response.observe(this, Observer { meme ->
val list = meme.data?.memes
adapter.submitList(list)
binding.recyclerView.adapter = adapter
})
viewModel.loading.observe(this, Observer { loading ->
binding.progressBar.isVisible = loading
})
viewModel.failed.observe(this, Observer { failed ->
binding.textViewFailed.text = failed
binding.textViewFailed.isVisible = true
})
}
}
adapter = MemesAdapter(MemesAdapter.OnClickListener { photo ->
Toast.makeText(applicationContext, "${photo.name}", Toast.LENGTH_SHORT).show()
})
演示屏幕
一旦完成,运行该应用程序。以下是你应该期待的。


在GitHub上查看整个项目。
结论
这还不是关于Recyclerviews 点击的全部,请继续探索。通过这篇文章,我希望你已经了解了如何处理你的Recyclerviews 上的点击。