최근 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에 올린 이 프로젝트를 확인해보시면 좋을것 같다.