Multi-View Types RecyclerView

jericho·2024년 2월 17일

Android

목록 보기
11/15

멀티뷰 코드 구조를 최적화한다고 정말 오랫동안 좋은 방법을 찾아봤지만, 찾지 못했다.
결국 내가 하던 sealed interface + enum 조합이 현재로선 가장 나은 것 같다.
뷰홀더를 상속해서 상위뷰홀더를 만들어 onBind를 추상화시키는 것도 결국 비슷한 품이 들어가는 것 같다. onCreate 쪽도 마찬가지. ViewType을 레이아웃 아이디로 넣는 방법도 봤지만 결국 그게 그거다.
마지막에 솔깃한 방법으로 commonViewHolder를 sealed class (혹은 interface)로 구성하는 것을 봤는데, 코드 전체를 뜯어고쳐야 해서 일단 현재 방법으로 포스팅을 작성한다.

데이터를 담는 sealed interface

//
sealed interface SealedMulti {

    enum class Type {
        HEADER,
        POPULAR,
        CATEGORY,
        VIDEO,
        LOADING,
    }

    val viewType: Type

    data object Header : SealedMulti {
        override val viewType: Type = Type.HEADER
    }

    data class Popular(
        val videoAdapter: PopularVideoAdapter
    ) : SealedMulti {
        override val viewType: Type = Type.POPULAR
    }

    data class Category(
        val categoryAdapter: CategoryAdapter,
    ) : SealedMulti {
        override val viewType: Type = Type.CATEGORY
    }

    data class Video(
        val videoItemData: VideoItemData,
    ) : SealedMulti {
        override val viewType: Type = Type.VIDEO
    }

    data object Loading : SealedMulti {
        override val viewType: Type = Type.LOADING
    }

}

뷰타입과 중첩 어댑터, 필요한 데이터 등을 실드 인터페이스로 묶어 관리해준다.

어댑터의 뷰홀더 타입

class HomeAdapter(
    private val onCategorySettingClick: () -> Unit,
    private val onVideoClick: (item: VideoItemData) -> Int?
) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {

파라미터는 필요한 람다식을 받으면 되고, 어댑터의 뷰홀더 타입으로는 공통되는 타입을 설정한다.
(리스트 어댑터로 바꾸는 것도 하고 싶었는데 시간 상 놔뒀다)

뷰홀더 클래스들

//
    inner class HeaderHolder(binding: ItemHeaderBinding) :
        RecyclerView.ViewHolder(binding.root)

    inner class PopularHolder(private val binding: HomeItemPopularBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun onBind(item: SealedMulti.Popular) = with(binding) {

            rvPopularVideos.adapter = item.videoAdapter
            rvPopularVideos.orientation = ViewPager2.ORIENTATION_HORIZONTAL
        }
    }

    inner class CategoryHolder(private val binding: HomeItemCategoryBinding) :
        RecyclerView.ViewHolder(binding.root) {

        init {
            binding.btnFlCategorySetting.setOnClickListener { onCategorySettingClick() }
            tvCategoryEmptyText = binding.tvCategoryEmptyText
        }

        fun onBind(item: SealedMulti.Category) = binding.also { b ->
            b.rvCategoryCategories.adapter = item.categoryAdapter
            b.tvCategoryEmptyText.isVisible = item.categoryAdapter.itemCount == 0
        }
    }

    inner class VideoHolder(private val binding: VideoItemBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun onBind(item: SealedMulti.Video) = binding.also { b ->

            item.videoItemData.videoThumbnailUri?.let {
                b.ivItemVideo.load(it) {
                    crossfade(true)
                    allowHardware(true)
                    if (position < 5) diskCachePolicy(CachePolicy.ENABLED)
                    else memoryCachePolicy(CachePolicy.ENABLED)
                }
            }
            item.videoItemData.channelThumbnailUri?.let {
                b.ivItemChannel.load(it) {
                    crossfade(true)
                    allowHardware(true)
                }
            }
            item.videoItemData.title?.let { b.tvItemTitle.text = it }
            item.videoItemData.channelName?.let { b.tvItemName.text = it }
            item.videoItemData.views?.let { b.tvItemViews.text = it }
            item.videoItemData.date?.let { b.tvItemDate.text = it }
            item.videoItemData.length?.let { b.tvPopularItemLength.text = it }

            b.root.setOnClickListener {
                onVideoClick(item.videoItemData)
            }
        }
    }

    inner class LoadingHolder(private val binding: ItemLoadingProgressBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun onBind() {
            binding.root.isVisible = sealedMultis.size > 4
        }
    }

뷰홀더 클래스들을 작성해준다.
inner class여야 어댑터에서 받은 람다식들을 사용할 수 있다.
(이거 때문에 외부 실드 인터페이스에 빼려고 하다가 되돌림)

getItemViewType

//
    override fun getItemCount(): Int = sealedMultis.size
    override fun getItemViewType(position: Int): Int = sealedMultis[position].viewType.ordinal

getItemViewType은 sealed interface와 enum ordinal을 사용해서 이렇게 간단하게 된다.

onCreateViewHolder, onBindViewHolder

//
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (SealedMulti.Type.values()[viewType]) {
            SealedMulti.Type.HEADER -> HeaderHolder(
                ItemHeaderBinding
                    .inflate(LayoutInflater.from(parent.context), parent, false)
            )

            SealedMulti.Type.POPULAR -> PopularHolder(
                HomeItemPopularBinding
                    .inflate(LayoutInflater.from(parent.context), parent, false)
            )

            SealedMulti.Type.CATEGORY -> CategoryHolder(
                HomeItemCategoryBinding
                    .inflate(LayoutInflater.from(parent.context), parent, false)
            )

            SealedMulti.Type.VIDEO -> VideoHolder(
                VideoItemBinding
                    .inflate(LayoutInflater.from(parent.context), parent, false)
            )

            SealedMulti.Type.LOADING -> LoadingHolder(
                ItemLoadingProgressBinding
                    .inflate(LayoutInflater.from(parent.context), parent, false)
            )
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (val item = sealedMultis[position]) {
            SealedMulti.Header -> Unit
            is SealedMulti.Popular -> (holder as PopularHolder).onBind(item)
            is SealedMulti.Category -> (holder as CategoryHolder).onBind(item)
            is SealedMulti.Video -> (holder as VideoHolder).onBind(item)
            SealedMulti.Loading -> (holder as LoadingHolder).onBind()
        }
    }

onCreate의 경우는 Int로 들어오는 뷰타입을 이넘.values()[viewType] 으로 exhaustive 하게 만들 수 있다.
(팩토리 등으로 통합시켜도 결국 뷰타입 확인해서 각 뷰홀더를 각 뷰바인딩으로 생성해줘야 하기에 어쩔 수 없는 것 같다. parent를 넘겨서 뷰홀더에서 직접 binding을 생성하게끔도 시도해봤지만 뷰홀더 클래스 상속 시 root view를 넘겨줘야 하기 때문에 결국 불가능했다.)

onBind의 경우는 실드 인터페이스로 exhaustive하게 된다.
(공통 뷰홀더를 만들어서 onBind 상속으로 통합하는 방법이 대표적인데, onBind가 필요 없는 헤더도 있고, item을 받지 않아도 되는 로딩도 있고, 그것 말고도 공통 뷰홀더 만들고 각 뷰홀더 클래스를 생성할 때 타입에 아이템 타입 일일이 집어넣어야 하고, onBind override 할 때도 타입 맞춰 넣어야 하고 해서 결국 드는 품은 현재 방법이 나은 것 같다.)

(작성하다보니 onBind에서 어댑터 계속 넣어주는거 개선해야 할 것 같은데 실드 인터페이스로 들어오는 어댑터를 onBind 외에 어떻게 접근할 지 모르겠다.)

 

이렇게 멀티뷰타입 리사이클러뷰를 정리해봤다.
(더 좋은 방법이 있다면 부디 조언해주시면 감사하겠습니다.)

참고

0개의 댓글