RecyclerView ViewBinder 사용으로 중복 코드 줄이기

송규빈·2024년 2월 15일
1
post-thumbnail
post-custom-banner

개요

기존의 다른 화면의 코드들을 본 뒤 개발을 하다가 비효율적인 면이 보이게 되어, 새롭게 개발한 화면과 기존 화면의 리팩토링을 한 내용을 조금 적어보려고한다.

여러 화면을 마주하다 보면 한 개의 리싸이클러뷰 어댑터에서 여러 뷰홀더를 관리해야 하는 상황이 온다.

중복되는 코드들

위와 같이 피드에 상점(FeedStore), 멤버(FeedMember), 상품(FeedItem)을 각각 표시하는 리스트 형식의 UI가 구성되어있다고 하자

Adapter

아래는 RecyclerViewAdapter 부분 코드이다.

class FeedAdapter : ListAdapter<Feed, ViewHolder>(DiffUtilCallback()) {

    override fun getItemViewType(position: Int): Int {
        val item = currentList.getOrElse(position) { return FeedType.FEED_ERROR.ordinal }
        return when (item) {
            is FeedItem -> FEED_ITEM.ordinal
            is FeedMember -> FEED_Member.ordinal
            is FeedStore -> FEED_Store.ordinal
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return viewHolderFactory.createViewHolder(parent, viewType)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = currentList.getOrElse(position) { return }
        when (item) {
            is FeedItem -> bindFeedItem(holder, item)
            is FeedMember -> bindFeedMember(holder, item)
            is FeedStore -> bindFeedStore(holder, item)
            else -> return
        }
    }

    /**
     * [FeedItem]을 뷰홀더에 bind
     *
     * @param holder [FeedItemViewHolder]
     * @param item [FeedItem]
     */
    private fun bindFeedItem(holder: ViewHolder, item: FeedItem) {
        val feedItemHolder = holder as? FeedItemViewHolder ?: return

        feedItemHolder.bind(item.uiModel)
    }

    /**
     * [FeedMember]를 뷰홀더에 bind
     *
     * @param holder [FeedMemberViewHolder]
     * @param item [FeedMember]
     */
    private fun bindFeedMember(holder: ViewHolder, item: FeedCurationItem) {
        val feedMemberHolder = holder as? FeedMemberViewHolder ?: return

        feedMemberHolder.bind(item.uiModel)
    }

    /**
     * [FeedStore]를 뷰홀더에 bind
     *
     * @param holder [FeedStoreViewHolder]
     * @param item [FeedStore]
     */
    private fun bindFeedStore(holder: ViewHolder, item: FeedCurationMember) {
        val feedStoreHolder = holder as? FeedStoreViewHolder ?: return

        feedStoreHolder.bind(item.uiModel)
    }
}

ViewHolder Factory

또한 뷰홀더 팩토리에서는 아래와 같이 뷰홀더를 viewType에 맞게 각각 생성해 줄 것이다.

class HomeFeedViewHolderFactory {
    fun createViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            FEED_ITEM.ordinal -> createFeedItemViewHolder(parent)
            FEED_MEMBER.ordinal -> createFeedMemberViewHolder(parent)
            FEED_STORE.ordinal -> createFeedStoreViewHolder(parent)
            else -> createErrorViewHolder(parent)
        }
    }

여기까지는 별 문제가 없어보인다. 하지만, 뷰홀더 내부를 들여다보면 비효율적인 면이 보인다.
상품, 회원, 상점 뷰홀더 모두 회원에 대한 닉네임을 표시하고 있다.
(예시 그림에서의 김짱구, 홍길동)

중복의 시작,,,

class FeedItemViewHolder(
    private val binding: AdapterFeedItemBinding,
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item:FeedItemUiModel){
        binding.executeAfter {
            setNickName(textNick, item.nick)
        }
    }
    /**
     * 판매자 닉네임 세팅
     *
     * @param view [AppCompatTextView]
     * @param nick 판매자 닉네임을 나타내는 String 값
     */
    fun setNickName(view: AppCompatTextView, nick: String) {
        view.text = nick
    }
}

class FeedMemberViewHolder(
    private val binding: AdapterFeedMemberBinding,
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item:FeedMemberUiModel){
        binding.executeAfter {
            setNickName(textNick, item.nick)
        }
    }
    /**
     * 판매자 닉네임 세팅
     *
     * @param view [AppCompatTextView]
     * @param nick 판매자 닉네임을 나타내는 String 값
     */
    fun setNickName(view: AppCompatTextView, nick: String) {
        view.text = nick
    }
}

class FeedStoreViewHolder(
    private val binding: AdapterFeedStoreBinding,
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item:FeedStoreUiModel){
        binding.executeAfter {
            setNickName(textNick, item.nick)
        }
    }
    /**
     * 판매자 닉네임 세팅
     *
     * @param view [AppCompatTextView]
     * @param nick 판매자 닉네임을 나타내는 String 값
     */
    fun setNickName(view: AppCompatTextView, nick: String) {
        view.text = nick
    }
}

동일한 기능을 가진 setNickName 메서드가 각 뷰홀더에서 중복되고 있다.

현재는 중복되는 메서드가 하나라서 괜찮아 보일 수 있겠지만, 뷰가 늘어나고 기능이 추가된다면 무시하지 못할만큼의 불필요한 작업들이 많아질 것이다.

해결방안

여러가지 고민 끝에 ViewBinder라는 클래스를 하나 만들어서 관리하기로 했다.
ViewBinder 클래스의 역할은 오로지 뷰에 데이터를 결합시키는 용도 즉, 데이터 렌더링을 돕는 메서드를 제공하는 역할만 수행되어야 한다.

class FeedViewBinder {

    /**
     * 판매자 닉네임 세팅
     *
     * @param view [AppCompatTextView]
     * @param nick 판매자 닉네임을 나타내는 String 값
     */
    fun setNickName(view: AppCompatTextView, nick: String) {
        view.text = nick
    }
}

이렇게 만든 FeedViewBinder를 각각의 뷰홀더에 주입하여 사용하면 된다.

class FeedItemViewHolder(
    private val binding: AdapterFeedItemBinding,
    private val feedViewBinder: FeedViewBinder
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item:FeedItemUiModel){
        binding.executeAfter {
            feedViewBinder.run {
                setNickName(textNick, item.nick)
            }
        }
    }
}

class FeedMemberViewHolder(
    private val binding: AdapterFeedMemberBinding,
    private val feedViewBinder: FeedViewBinder
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item:FeedMemberUiModel){
        binding.executeAfter {
            feedViewBinder.run {
                setNickName(textNick, item.nick)
            }
        }
    }
}

class FeedStoreViewHolder(
    private val binding: AdapterFeedStoreBinding,
    private val feedViewBinder: FeedViewBinder
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item:FeedStoreUiModel){
        binding.executeAfter {
            feedViewBinder.run {
                setNickName(textNick, item.nick)
            }
        }
    }
}
profile
🚀 상상을 좋아하는 개발자
post-custom-banner

0개의 댓글