RecyclerView를 사용하게 되면 대부분 클릭 이벤트를 처리해야하는 경우가 생긴다. 아이템을 클릭 했을 때, 아이템의 특정 버튼을 클릭 했을 때 등 여러가지 경우가 있다. 클릭 이벤트를 처리하기 위한 다양한 방법들을 알아보자.
class BlogViewHolder(
private val binding: ItemBlogBinding,
) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener{
// click event
}
}
fun bind(category: UiPost.Blog) {
with(binding){
blog = category
executePendingBindings()
}
}
}
위 코드는 BlogViewHolder에 정의한 클릭 리스너이다. 보통 아이템의 클릭 리스너는 ViewHolder가 생성될 때 정의한다. 만약, bind에 리스너를 정의하게 되면 아이템이 bind 될 때마다 리스너를 재정의 할 것이다.
**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView
init {
// Define click listener for the ViewHolder's View.
textView = view.findViewById(R.id.textView)
}
}
RecyclerView에 관한 구글 공식문서에서도 init
에 클릭 리스너를 정의한다고 적혀있다.
init {
itemView.setOnClickListener{
// click event
}
}
클릭 리스너를 정의하고 클릭 이벤트 처리를 위한 로직을 구현하게 되는데, 일반적인 경우는 원하는 동작을 리스너 안에 구현하면된다.
하지만, Adapter에서 처리할 수 없는 로직들이 있을 것이다. 예를 들어 Adapter를 가지고 있는 부모 View의 context가 필요하다던지 ViewModel에서 특정 로직을 수행해야하는 경우도 있을 것이다. context 나 ViewModel을 Adpater에 주입받는 것은 안티패턴이다. ViewModel의 경우 Adapter보다 생명주기가 더 길기 때문에 memory leak이 발생할 수 있다.
이러한 문제를 해결하기 위해 옵저버 패턴을 활용하여 클릭 했을 때 이벤트를 구독하고 있는 구독자에게 이벤트를 넘겨 원하는 동작을 수행하도록 할 수 있다.
가장 대표적으로 interface를 활용하는 방법이 있다. 옵저버 패턴을 활용할 수 있는 대표적인 방법으로 자바, 코틀린 상관없이 많이 사용되는 방식이다.
private lateinit var onItemClickListener: OnItemClickListener
interface OnItemClickListener {
fun onClick(blog: UiPost.Blog)
}
fun setOnItemClickListener(onItemClickListener: OnItemClickListener) {
this.onItemClickListener = onItemClickListener
}
먼저 Adapter에 OnItemClickListener
라는 인터페이스를 만들어 변수에 담아주는 함수를 정의한다.
class BlogViewHolder(
private val binding: ItemBlogBinding,
private val onItemClickListener: OnItemClickListener,
) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
binding.blog?.let { blog ->
onItemClickListener.onClick(blog)
}
}
}
...
}
클릭 시 onClick
함수를 실행하도록 하고 필요한 데이터를 넘겨준다. 이때 ViewHolder를 생성할 때 onItemClickListener
변수를 넘겨주도록 구현한다. ViewHolder를 inner class로 정의하면 Adapter에 있는 onItemClickListener
에 접근할 수 있지만, 이는 안티패턴이다. inner class로 정의하게 되면 outer class의 자원에 접근할 수 있으며 불필요한 정보를 가지고 있는 경우가 발생할 수 있고 이는 메모리 누수를 야기할 수 있다.
adapter.setOnItemClickListener(object : BlogAdapter.OnItemClickListener {
override fun onClick(blog: UiPost.Blog) {
// click event
}
})
Activity나 Fragment에서 클릭 리스너에 대한 이벤트 처리를 정의하면서 원하는 동작을 구현할 수 있다.
Rxjava를 사용하고 있다면 이를 이용해 클릭 이벤트 처리를 구현할 수 있다. interface를 활용하는 방법보다 코드가 좀 더 깔끔해진다.
val onItemClickSubject: PublishSubject<UiPost.Blog> = PublishSubject.create()
먼저 Adapter에 PublishSubject
변수를 선언하여 객체를 생성한다.
class BlogViewHolder(
private val binding: ItemBlogBinding,
private val onItemClickSubject: PublishSubject<UiPost.Blog>,
) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
binding.blog?.let { blog ->
onItemClickSubject.onNext(blog)
}
}
}
...
}
interface 방식과 동일하게 onItemClickSubject
변수를 ViewHolder를 생성할 때 매개변수로 넘겨준다.
onNext 메서드를 사용하여 이벤트를 발행한다.
val disposable = adapter.onItemClickSubject.subscribe { blog ->
// click event
}
compositeDisposable.add(disposable)
Activity나 Fragment에서 구독하므로 클릭 이벤트에 대한 처리가 가능하다. 구독하고 있는 스트림을 정리하기 위해 compositeDisposable
에 추가하여 뷰가 파괴될 때 정리한다.
함수형 프로그래밍 언어를 사용한다면 고차함수를 활용하여 클릭 이벤트를 처리할 수도 있다. 최근에 알게된 방식인데 나름 괜찮은 방식인 것 같다.
val blogAdapter = BlogAdapter(::show)
fun show(blog: UiPost.Blog) {
// click event
}
Activity나 Fragment에 정의된 함수가 있을 것이다. 이 함수를 Adapter의 인자로 전달한다.
class BlogAdapter(
private val onItemClick: (UiPost.Blog) -> Unit,
) {
override fun onCreateViewHolder() {
return BlogViewHolder(
..,
onItemClick
)
}
}
...
}
ViewHolder를 생성할 때 인자로 함수를 전달한다.
class BlogViewHolder(
private val binding: ItemBlogBinding,
private val onItemClick: (UiPost.Blog) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
binding.blog?.let { blog ->
onItemClick(blog)
}
}
}
...
}
정의된 아이템 클릭 리스너에서 함수를 호출한다.
다른 방식들과의 차이점은 Adapter의 인자로 전달하는 값이 있고 없고의 차이이다. 어느 방법이 더 좋다 라기 보다는 원하는 방식으로 통일성있게 구현하면 될 것이다.
참고자료
https://developer.android.com/guide/topics/ui/layout/recyclerview?hl=ko