다양한 Adapter Click 이벤트 처리

유재민·2023년 5월 8일
0
post-thumbnail

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

가장 대표적으로 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 PublishSubject

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

profile
유잼코딩

0개의 댓글