RecyclerView 각 아이템 이벤트를 효율적으로 핸들링할 수 있을까?

최근 RecyclerView를 구성하면서 가장 고민한 것이, 어떻게 하면 각 뷰의 이벤트들을 잘 핸들링 할 수 있을까에 대해서이다. 많은 RecyclerView를 만들며 고민하다가 정착한 방법이 있어 글로 남긴다. 다른 의견이나 좋은 방법이 있다면 댓글로 알려주시면 안드로이드 애플리케이션 개발 공부에 큰 도움이 될것 같다.

첫째로 이 문제를 고민하게 된 것은, 뷰와 관련된 모든 로직을 해당 RecyclerView를 가지고 있는 Fragment나 Activity의 ViewModel에서 처리하고 싶다는 생각에서 시작했다. 그러면서도 해당 RecyclerView에 완전히 종속하지 않고 계속 재사용 할 수 있는 ViewHolder를 설계하고 싶었다. 예전에는 몰랐었는데 RecyclerView의 ViewHolder와 Adapter는 데이터 형태만 어느정도 같다면 분명히 재사용 할 수 있는 여지가 있기 때문이다.

예시로 게시글 아이템과 광고 아이템이 공동으로 존재하는 RecyclerView를 구성하려고 한다.

일단 각 ViewHolder마다 어떤 이벤트를 가지고 있는지 정리한 후, 이를 인터페이스로 작성했다. 각 ViewHolder마다 이벤트를 굳이 분리한 이유는, 다른 Adapter를 가지는 RecyclerView에서도 해당 ViewHolder를 사용할 수 있도록 하기 위해서이다.

이 상태에서 ViewHolder는 완전히 독립적인 파트로 존재하게 된다. ViewModel을 각 아이템 뷰의 DataBinding의 변수로 받고 싶은데, 해당 변수의 타입을 고정해버리면 다른 어댑터에서는 사용을 위해 새로운 레이아웃 파일을 구성해야 하는 불편한 일이 일어난다.

class RegularPostViewHolder(private val binding: ItemRegularPostBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(regularPostViewHolderEvent: RegularPostViewHolderEvent, postData: PostData) {
            binding.apply {
                event = regularPostViewHolderEvent
                post = postData
            }
        }

        interface RegularPostViewHolderEvent {
            fun onPostSelected(postData: PostData)
        }
}

위 코드의 RegularPostViewHolderEvent가 해당 뷰 홀더 RegularPostViewHolder의 이벤트를 모두 담고 있는 인터페이스의 예시이다. RegularPostViewHolder의 bind 함수에서는 RegularPostViewHolderEvent를 매개변수로 받아서 DataBinding의 event 변수의 값으로 전달한다.

class AdViewHolder(private val binding: ItemAdBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(adViewHolderEvent: AdViewHolderEvent) {
            binding.apply {
                event = adViewHolderEvent
            }
        }

        interface AdViewHolderEvent {
            fun onAdSelected()
        }
}

위 코드의 AdViewHolderEvent가 해당 뷰 홀더 AdViewHolder의 이벤트를 모두 담고 있는 인터페이스의 예시이다. AdViewHolder의 bind 함수에서는 AdViewHolderEvent를 매개변수로 받아서 DataBinding의 event 변수의 값으로 전달한다.

다음과 같이 코드를 구성하면 ViewHolder는 각자에게 필요한 -ViewHolderEvent 인터페이스를 가지게 된다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="event"
        type="com.gomsang.recyclerviewexperiments.PostRecyclerViewAdapter.AdViewHolder.AdViewHolderEvent" />
    </data>
    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginRight="16dp"
        android:layout_marginBottom="8dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="@color/black"
            android:onClick="@{()->event.onAdSelected()}"
            android:orientation="vertical" />

    </androidx.cardview.widget.CardView>
</layout>

각 뷰 홀더의 레이아웃 파일에서 다음과 같이 데이터바인딩 변수로 자신의 ViewHolderEvent를 받고, android:onClick="@{()->event.onAdSelected()}" 과 같이 이벤트 발생시 해당 함수를 호출해 주면 된다.

이제 해당 RecyclerView의 Adapter에서 사용하는 ViewHolder들의 ViewHolderEvent 인터페이스를 모두 상속하여 해당 RecyclerView에서 발생하는 모든 이벤트에 대응하는 EventListener 클래스를 한번 더 생성할 것이다. 예를들면 다음과 같이 생성할 수 있다.

두가지 선택지가 있다. abstract class 로 작성하면 해당 클래스 선언시 상속할 클래스만 명시해 주면 된다. 다만 해당 클래스를 생성하는 쪽에서 객체 생성시 바디를 모두 작성해야 하므로 하나의 뷰 모델에서 해당 EventListener를 사용하는 경우, 즉 항상 모든 ViewHolderEvent에 대응해야 하는 경우 유리하다.

abstract class PostRecyclerViewEventListener : 
RegularPostViewHolder.RegularPostViewHolderEvent, AdViewHolder.AdViewHolderEvent

일반 클래스로 작성하면, ViewHolderEvent 클래스들을 상속시 모든 함수를 오버라이딩해줘야한다. (빈 body라도) 다만 이 경우 선언한 EventListener를 생성하는 경우 본인이 필요한 이벤트에만 대응할 수 있다. 그래서 많은 뷰홀더들이 혼합되어 있는 경우 조금 더 유리하다.

open class PostRecyclerViewEventListener :
 RegularPostViewHolder.RegularPostViewHolderEvent,
 AdViewHolder.AdViewHolderEvent {
        override fun onPostSelected(postData: PostData) {}
        override fun onAdSelected() {}
}

어쨌든 둘중 하나를 골라서 Adapter 클래스 안에서 EventListener를 구성해주면 된다!

이제 만든 EventListener를 RecyclerView밖에서 구성해줄 것이다. 이 프로젝트에서는 해당 리사이클러 뷰가 위치하는 액티비티의 뷰 모델에 위치시켰다.

class MainViewModel : ViewModel() {
    val eventListener = object : PostRecyclerViewAdapter.PostRecyclerViewEventListener() {
        override fun onPostSelected(postData: PostData) {
            Log.d("MainViewModel", "onPostSelected: ${postData.title}")
        }
        override fun onAdSelected() {
            Log.d("MainViewModel", "onAdSelected")
        }
    }
}

이렇게 구성한 EventListener는 어댑터 생성시에 인자로 전달하면 된다.

val adapter = PostRecyclerViewAdapter(viewModel.eventListener)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is RegularPostViewHolder) holder.bind(
            recyclerViewEventListener,
            /* POST */
        )
        if (holder is AdViewHolder) holder.bind(recyclerViewEventListener)
}

해당 ViewHolder를 바인딩 할 때, Adapter를 생성할 때 지정했던 EventListener를 인자로 전달하게 되고 결과적으로 각 뷰들의 이벤트를 상위 ViewModel에서 재구성하여 핸들링 할 수 있다.

가장 큰 장점은 각 ViewHolderEvent들을 상속하여 EventListener를 구성하였기 때문에, 이후 각 ViewHolderEvent의 함수가 변경되거나 삭제되는 경우 Android Studio의 Find Usages 와 같은 기능으로 빠르게 찾을 수 있다. 또한 ViewHolderEvent가 변경되는 경우 어떤식으로든 빌드시 Syntax 에러가 나타날 것이므로 기능별 관리에 유용하다.

지금까지 느낀 단점 중 하나는 여러개의 Event들을 상속하다 보니 각 Event의 함수 이름이 중복 되는 경우 EventListener 구성이 어렵다는 것이다. 다만 이 부분은 함수에 식별문(데이터 이름이나 뷰홀더 이름)등을 포함하면 쉽게 해결할 수 있는 부분이므로 크게 문제가 되지는 않는것 같다.

이 글에서 말씀드린 방법이 관심이 있다면, Github에 올린 이 프로젝트를 확인해보시면 좋을것 같다.

https://github.com/gomsang/RecyclerViewEventHandlingExample

Leave a comment

Your email address will not be published. Required fields are marked *