RecyclerView.Adapter 어떻게 접근하면 좋을까? - ViewModel



개인 광고 영역

RecyclerView와 ListView을 통해 수많은 형태의 List를 생성할 수 있다. 동일한 형태의 View을 구현하거나, N 개의 서로 다른 View을 만들기도 한다.

리스트를 구성하는데 유용한 RecyclerView. 이런 RecyclerView을 구성하는데 있어 MVP 또는 MVVM에서 적용해볼 수 있는 방법은 무엇일까?

이번 글에서는 필자가 선택한 ViewModel을 소개하려고 하는데 아무래도 다음의 글이 영향을 미쳤기에 작성하는 글이다.

RecyclerView는 레이아웃을 구성하는 LayoutManager 들과 RecyclerView.Adapter을 통한 View을 구성할 수 있다. 그 Adapter에 데이터를 포함하게 된다. Data의 형태도 개발자가 쓰고 싶은 데로 쓸 수 있는 장점을 가진다.(ListView에서는 기본 List이다)

RecyclerView

하지만 이러한 RecyclerView는 어딘가 모르게 짬뽕의 코드로 보이는데, 아래와 같이 간단하게 구현하는 게 가능하다.

class SimpleAdapter(private val context: Context, private val list: ArrayList<String>) : RecyclerView.Adapter<SimpleAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): SimpleAdapter.ViewHolder {
        return ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_text_sample, parent))
    }

    override fun getItemCount() = list.size

    override fun onBindViewHolder(holder: SimpleAdapter.ViewHolder?, position: Int) {
        holder?.itemView?.run {
            tv_message.text = "Set TEXT"
        }
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}

하지만 ViewHolder가 하나만 존재할리는 없다. 많으면 수십 개도 생성될 수 있으며, ViewType을 정의해서 N 개의 View 활용이 가능한 형태로 만들 수 있다.

class SimpleAdapter(private val context: Context, private val list: ArrayList<String>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        const val TYPE_ONE = 1
        const val TYPE_TWO = 2
        const val TYPE_THREE = 3
    }

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            // ...
            else -> ViewHolderThree(LayoutInflater.from(context).inflate(R.layout.item_text_sample, parent))
        }
    }

    override fun getItemCount() = list.size

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
        when (holder) {
            is ViewHolderOne -> {
                holder.itemView?.run {
                    tv_message.text = "ViewHolder One"
                }
            }
            // ... 생략
        }

    }

    override fun getItemViewType(position: Int): Int {
        return when (position) {
            0 -> TYPE_ONE
            1 -> TYPE_TWO
            else -> TYPE_THREE
        }
    }

    class ViewHolderOne(itemView: View) : RecyclerView.ViewHolder(itemView)
    class ViewHolderTwo(itemView: View) : RecyclerView.ViewHolder(itemView)
    class ViewHolderThree(itemView: View) : RecyclerView.ViewHolder(itemView)
}

ViewHolder가 3개가 존재한다면 위와 같을 것이다. 위와 같이 구분해두면 점점 코드 양은 길어진다. onClick 처리도 그에 따라 함께 길어지며, list에 아이템을 추가하거나, 빼거나, 지우거나 하는 등의 모든 코드가 필요하다.

단순히 list을 밖에서 호출해서 처리하고, notifyDataSetChanged() 처리하는 것도 가능하겠지만 뭔가 깔끔하지 않다.

먼저 ViewHolder을 분리하고, 필자가 했던 ViewModel을 분리해보려고 한다.


ViewHolder을 각자가 처리

ViewHolder가 계속 추가되어야 하며, 각각의 코드 양이 수십에서 수백 줄이 넘는다고 가정하자. 이 경우 Adapter에서 위와 같은 코드로 작업한다면 당연히 코드는 기하급수적으로 늘어날 수밖에 없다. 그리고 수정하는 것조차 힘들어진다.

그래서 필자는 BaseViewHolder을 아래와 같이 구성하여 사용하고 있다.

abstract class BaseViewHolder<in ITEM : Any>(
        val context: Context,
        @LayoutRes layoutRes: Int,
        parent: ViewGroup?)
    : RecyclerView.ViewHolder(LayoutInflater.from(context).inflate(layoutRes, parent, false)) {

    fun onBindViewHolder(item: Any?) {
        try {
            onViewCreated(item as? ITEM?)
        } catch (e: Exception) {
            itemView.visibility = View.GONE
        }
    }

    abstract fun onViewCreated(item: ITEM?)
}

그에 따라 BaseRecyclerViewAdapter을 생성하고, 아래와 같은 코드를 호출하도록 만들었다.

abstract class BaseRecyclerAdapter(val context: Context?)
    : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

      // 생략

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
        (holder as? BaseViewHolder<*>)?.onBindViewHolder(getItem(position))
    }

    // 생략
}

위와 같이 정의하고, BaseViewHolder을 상속받아 ViewHolder을 구현하고, BaseRecyclerAdapter을 통해 구현해야 할 부분을 최소화시켰다.

BaseRecyclerAdapter을 상속받아 구현해야 할 부분은 onCreateViewHolder 부분만 정의하도록 수정한 것이다. 그리고 레이아웃도 각자의 ViewHolder가 직접 구성한다.

override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
    return when (viewType) {
        TYPE_ONE -> ViewHolderOne(context, R.layout.item_text_sample, parent)
        TYPE_TWO -> ViewHolderTwo(context, R.layout.item_text_sample, parent)
        else -> ViewHolderThree(context, R.layout.item_text_sample, parent)
    }
}

처음 Adapter 보다 간결한 코드가 만들어졌고, 이를 잘 활용하는 게 가능해졌다. 뭐 아직은 MVP/MVVM 과는 무관하지만…


ViewModel의 역할은?

필자가 ViewModel을 도입한 이유는 간단하다, Adapter의 구현체를 최소화시키길 원했으며, 더 나아가 Adapter을 별도 클래스로 만드는 게 아니라 View 쪽에서 ViewType에 따른 ViewHolder 생성 코드 onCreateViewHolder 만을 구현하길 원하기 때문이다.

쉽게 말해 어차피 짧아진 Adapter 구현체를 최소화시키길 원하는 궁극적인 목표가 있기 때문이다.

그래서 ViewModel를 통해서 Data을 관리하고, ViewHolder에서 필요한 컴포넌트를 적용하며, ViewModel만 교체하면 언제든 다른 RecyclerViewAdapter로 변신하기를 원하는 것이다.(뭐 Adapter 상속 구조만 변경해도 괜찮겠지만)

손대는 코드를 최소화 시켜보자는 의미로 ViewModel을 만든것이다.


ViewModel을 구성해보자!

그래서 탄생한 ViewModel은 어딘가 모르게 Adapter에서 하던 대부분의 일을 그대로 가져오게 되었다. 그리고 ViewHolder는 이 ViewModel의 interface 정의를 그대로 가지고 있도록 하였다.

ViewType을 좀 더 유연하게 가지도록 하려고, 아래의 sealed class 정의를 두었다.

sealed class ViewHolderSealedData

data class ViewHolderInfo(
        val viewType: Int) : ViewHolderSealedData()

대략 아래와 같이 ViewModelAdapter을 구성하였다. 특히 sealed class은 나중에 header/footer 등의 데이터 추가가 필요한 경우 유용하게 확장하기 위해서 사용하였다. 그리고 ViewHolderInfo 역시 필요한 데이터를 확장하는 게 가능하다. 특히 ViewHolder을 열고 닫기가 필요한 경우에도 유용하게 확장이 가능하다.

open class BaseAdapterViewModel(override val context: Context?) : BaseAdapterNavigator {

  protected val itemList = mutableListOf<Pair<Any?, ViewHolderSealedData>>()

  override fun getItemViewType(position: Int): Int {
        val item = getPairItem(position)
        return when (item) {
            is ViewHolderInfo -> item.viewType
            else -> -1
        }
    }

    override fun addItem(item: Any?, viewType: Int) {
        itemList.add(Pair(item, ViewHolderInfo(viewType)))
    }

    override fun clear() {
        itemList.clear()
    }

    override fun getItemCount() = itemList.size
}


ViewModel을 통해 얻는 점은?

당장은 크게 없다. 필요한 경우 BaseAdapterViewModel 자체를 확장하여 사용 가능하다.

Adapter을 완전하게 분리했다는 장점은 분명 존재한다. Adapter 역할은 View의 껍데기를 생성하는 역할이 전부이다.

그래서 이 어댑터에 ViewModel을 어떤 식으로 접근할지에 따라서 언제든 데이터 구성을 변경하는 게 가능하다. 기본 BaseAdapterViewModel을 잘만 활용하면 모든 게 가능하다.

그리고 BaseAdapterViewModel에 대한 UnitTest을 통해 Adapter 본연의 안정성 확보도 가능하다고 생각한다.

이렇게 확장할 수 있었던 것 자체도 SeongUg Steve Jung(GDE) 님이 다루었던 Adapter, 누구냐 넌? — Data? View? 글 덕분이긴 하다.

결국 이렇게 Adapter에 ViewModel을 완전히 분리한 이유는 확장성, 단순하게 Adapter에서 하는 역할을 축소하는 게 주 목적이긴 하다.

반복 코딩하는 게 귀찮아서가 더 정답에 가깝지만…


Adapter와 ViewModel의 확장 가능성?

현재로서 확장 가능성 부분은 Adapter 자체를 상속받아서 구현하는 부분을 전부다 없애는 것이다. 더 간단하게 아래와 같은 형태로 만들어볼 수 있을 것 같다.

private val adapter: BaseRecyclerAdapter by lazy {
    object : BaseRecyclerAdapter(this, BaseAdapterViewModel(this)) {
        override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
            // 생략
        }
    }
}

이미 가능하긴 한데 아직 정리를 하지 못해서 시도는 못했다. Kotlin Higher-Order function을 통해 이를 도입할까 하다가, 굳이 그럴 필요는 없을 것 같다는 생각이 든다.

생성과 동시에 BaseAdapterViewModel도 함께 생성할 수 있도록 구체화한다. 아무래도 dagger을 통한 접근 방식은 좀 다를 것 같지만 코드를 줄일 수 있다는 장점은 존재한다.

그리고 그에 따른 ViewHolder는 알아서 구성할 수 있다.

단순화하는 게 가능해졌지만, itemList에 아이템을 자유롭게 추가하고, 빼는 기능이 필요할 수 있다.

네트워크를 통해 특정 영역에 아이템 전부를 추가하는 경우도 분명 필요하다. 이런 경우를 대비해서 Map을 활용할까를 생각하고 있는데 아직 계획만 가지고 있다.

아이템 자체도 분명 자유롭게 접근하는 게 가능해야 하는 경우가 있어서 작업을 진행해야 한다.


View의 갱신과 onClick 처리는?

ViewModel을 구현하였는데 이러한 ViewModel을 통해 View의 갱신과 onClick을 하도록 만들고 있다.

필자의 경우는 아래와 같은 2가지 형태를 모두 사용하고 있는데 보통 kotlin Higher-Order function을 이용하고 있다.

/**
 * 아이템 클릭 처리
 * @param Int : position
 */
var onClickItem: (Int) -> Unit

// Or

/**
 * 아이템 클릭 처리
 * @param Int : position
 */
val onClickItem: Observable<Int>

보통 Higher-Order function을 ViewModel에 정의해두고, ViewHolder init {} 부분에 아래와 같이 onClick을 처리하도록 하고 있다.

init {
    itemView.setOnClickListener {
      viewModel.onClickItem(adapterPosition)
      // or
      viewModel.onClickItem.onNext(adapterPosition)
    }
}

위와 같이 ViewHolder가 생성됨과 동시에 onClickItem 처리를 하고 있다.

2017년 All Abtout Kotlin 행사에서 Seungmin Lee 님이 발표하신 뷰 실시간으로 동기화 하기 부분을 참고하는 것도 좋은 방법이다.


마무리

이러한 접근 방법이 깔끔한 것은 아니다.

다만 ViewModel의 ViewModelNavigator(interface)를 통해서 ViewHolder에 접근할 수 있는 부분을 제한하고, 테스트 가능한 형태로 만들 수 있을 것 같은 기대는 있다.

이런 접근 방법은 아직은 진행 중이라 완전하지 않다. 그리고 완전할 수도 없다.

여하튼 조금이나마 괜찮은 접근 방법과 Adapter을 어떤 식으로 분리하면 좋을까를 고민하여 이 글을 공유해본다.

더 좋은 방법이 있다면 필자와 함께 고민을 함께 하면 좋을 것 같으니 페북으로 댓글을 남겨주면 좋을 것 같다!.


GDG Korea Android 발표자료



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments