Android RecyclerView Adapter에 DataBinding 활용하기



개인 광고 영역

Android RecyclerView.Adapter에 DataBinding 활용하는 방법을 정리한다.

Android RecyclerView 사용하기 Android ListView 사용하기

이번에 작성하는 글에는 Android ViewModel을 활용하며, DataBinding을 간단하게 활용하기 위한 방법을 소개한다.


알아보기 전에

이번 글에서는 아래 내용을 토대로 작성하지만, 이미 DataBinding 활용을 잘 하고 있는 사용자는 불필요한 내용일 수 있다.

  • Base로 DataBinding을 활용하는 Adapter를 활용하는 방법을 소개한다.
  • 1개 이상의 ViewHolder를 사용하고, DataBinding을 활용하려는 경우
  • Jetpack Android ViewModel 사용방법을 알고 있다.
  • RecyclerView.Adapter 활용 방법을 잘 알고 있다.

이 라이브러리는 GitHub에서 확인 가능하다.

SimpleAdapter - GitHub


1개 이상의 ViewHolder와 Item을 사용하는 방법

1개 이상의 ViewHolder를 사용하는 경우는 매우 흔하고, 아이템도 서로 다른 경우가 많다.

이 경우 대부분은 Any(Object) 타입을 정의해서 아이템을 List에 저장하고, cast 해서 사용한다.

이미 만들어진 좋은 라이브러리를 활용하는 방법 역시 가능하다.

이 두 개의 방법은 item을 Any로 두고 cast 할 필요 없이 사용이 가능하다.


추가로 DataBinding을 활용한 라이브러리는?

AirBnb에서 만들어둔 Epoxy library - GitHub를 활용할 수 있다.

매우 간단하게 DataBinding을 활용하여 View 초기화와 데이터를 주입하는 방식으로 사용하는 Adapter이다.

단점이라면 DataBinding을 정의한 xml 작업이 끝나면 재 빌드를 해야 하는 단점이 있으나, 이를 제외하면 매우 편한 라이브러리이다.


DataBinding 이란?

DataBinding - link은 레이아웃의 UI 구성 요소를 프로그래밍 방식(findViewById 등) 형태가 아닌 선언적 형태를 사용하여 앱의 데이터 소스에 바인당 할 수 있는 지원 라이브러리이다.

The Data Binding Library is a support library that allows you to bind UI components in your layouts to data sources in your app using a declarative format rather than programmatically.

아래와 같이 findViewById를 하고, 데이터를 입력하는 방식 대신

findViewById<TextView>(R.id.text_view).run {
  text = "text"
}

xml의 상단에 data를 정의하고, 이를 아래와 같이 활용할 수 있다. @{data 정의}

<layout>
    <data>
        <variable
            name="item"
            type="java.lang.String" />
    </data>
    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@{item}" />
</layout>


DataBinding 적용하기

DataBinding - link은 Android 4.0 이상부터 사용이 가능한 라이브러리다.

개발 중인 App build.gradle에 아래처럼 dataBinding을 추가한다. kotlin 사용 시 꼭 kapt도 함께 추가해야 한다.

다음 코드처럼 정의하여 활성화한다.

plugins {
    id 'kotlin-android'
    id 'kotlin-kapt'
}

android {
    ...
    dataBinding {
        enabled = true
    }
}


DataBinding xml에 정의

DataBinding을 사용하기 위해 xml 정의가 필요한데, 으로 시작해 으로 끝나는 가운데 크게 2개의 블록을 정의해야 한다.

  • data : 사용할 data의 variable 정의. import가 필요한 경우 이 영역에 정의한다.
  • layout : 화면에 보일 layout 정의
<layout>
  <!-- data 정의 -->
  <data>
    <variable name="obj" type="Item"/>
  </data>
  <!-- layout 정의 -->
</layout>


일반적인 ViewHolder에서 사용하는 DataBinding 정의

DataBinding을 활용해 일반적인 ViewHolder 정의는 아래와 같은데,

layout 이름을 simple_temp_item.xml로 정의했다면 DataBinding에 의해 생성한 데이터는 SimpleTempItemBinding.java로 구현되어 있다.

실제 DataBinding 해야 할 객체는 SimpleTempItemBinding을 초기화해야 한다. SimpleTempItemBinding의 root(View)를 RecyclerView.ViewHolder에 넘겨준다.

class SampleViewHolder(
    private val viewBinding: SimpleTempItemBinding
) : RecyclerView.ViewHolder(
    viewBinding.root
) {

    /**
     * ViewBinding single viewModel and item binding.
     */
    fun bindViewHolder(item: Any) {
        viewBinding.setItem(item)
        viewBinding.executePendingBindings()
    }
}


Base로 정의해보자

그럼 Base 형태로 만들어두고 사용하려면 어떻게 해야 할까. 답은 ViewDataBinding으로 Generic type 정의를 통해 해결 가능하다.

하지만 필자가 만든 라이브러리는 아래와 같은 조건을 가지고 만들었다.

SimpleAdapter - GitHub

  • onCreate 시점에 ViewHolder는 생성만 한다.
  • onBindViewHolder에서는 별도로 세팅할 부분이 없어야 한다.
  • 상속받아 binding을 처리할 필요 없어야 한다.
  • 단순 아이템 셋을 통해서만 처리하고 싶다.

이 조건에 맞는 ViewHolder와 Adapter를 각각 살펴보자.


ViewHolder 정의

ViewHolder는 아래와 같이 정의만 하고, 추후 상속을 받거나 확장해 사용하지 않도록 만들었다.

SimpleDataBindingViewHolder을 확장 불가능한 형태로 만들고, ViewDataBinding을 받는다. 이때 Generic type을 정의하지 않는다.

그냥 있는 그대로 초기화 시키고, setVariable 하도록 만들었다.

결국 그냥 Any 형태로 넘기고 끝내도록 했다. 그러다 보니 간단하게 처리가 가능하다.

여기서 중요한 건 공용으로 사용하기 위해서 item과 viewModel을 정의했다는 부분이다.

  • viewModel : 대부분의 onClick 또는 양방향 데이터 처리를 한다.
  • item : item을 설정한다.
class SimpleDataBindingViewHolder(
    private val viewBinding: ViewDataBinding
) : RecyclerView.ViewHolder(
    viewBinding.root
) {

    /**
     * ViewBinding single viewModel and item binding.
     */
    fun onBindViewHolder(viewModel: Any?, item: Any?) {
        viewModel?.let {
            viewBinding.setVariable(BR.viewModel, it)
        }
        item?.let {
            viewBinding.setVariable(BR.item, it)
        }
        viewBinding.executePendingBindings()
    }
}

하지만 저 BR은 layout 정의가 있어야 자동으로 만들어진다. 그러다 보니 약간의 꼼수를 통해 다음을 만들었다.

simple_temp_item

<layout>
    <data>
        <variable name="viewModel" />
        <variable name="item" />
    </data>
</layout>

이렇게 2개의 변수를 만들어두고, 재빌드하면 BR.viewModel, BR.item 2개가 만들어져있고 사용할 수 있다.

  • 장점 : 공용으로 사용하는 게 가능하다.
  • 단점 : item과 viewModel을 제외하면 사용하는 게 불가능하다.

item과 viewModel 하나씩만 사용가능한데, 만약 ViewModel이 2개 필요하다면 어떻게 할 수 있을까?

  • item에 viewModel을 추가 정의한다.
  • viewModel에는 기본으로 정의하고, item에 추가로 viewModelTwo를 정의해 사용한다.

방법은 여러 개가 있을 것 같으나, 위와 같이 생각해보았다. 그중 첫 번째 방법을 아래와 같이 사용하는 게 가능하다.

data class TempItem(
  item: Something,
  viewModelOne: ViewModelOne,
  viewModelTwo: ViewModelTwo
)


Adapter와 데이터는?

ViewHolder는 위와 같이 정의했는데, 그럼 Adapter와 Data 처리는 어떻게 해야 할까?

N 개의 ViewHolder 패턴에서 사용했던 데이터 Any는 유지해야 한다. 대신 item과 ViewModel을 nullable로 처리하고, layout 정의에 따라 움직이게 처리한다.

단 nullable이어야, layout 정의대로 움직인다.

data class SimpleDataBindingItem(
    val viewType: Int,
    val item: Any?,
    val viewModel: Any? = null
)

어댑터는 일반적인 어댑터와 크게 다르지 않다. 다만 필자는 밖에서 쉽게 접근할 수 있도록 Higher-Order function을 통한 초기화가 가능하도록 만들었다.


SimpleDataBindingAdapterControl

어댑터를 감싸기 위해서 AdapterControl을 하나 추가했다. Adapter를 내부적으로 초기화하고, 꼭 필요한 데이터 정의와, View 정의만 하도록 만들었다.

조금 복잡해 보이나. 접근을 제한하기 위해서 SimpleDataBindingAdapterCreateItem을 사용한다. SimpleDataBindingAdapterCreateItem에는 ViewGroup과 ViewType 2개를 변수로 전달한다.

setItems을 통해 정의한 아이템 정보는 onCreateItems을 통해 콜백 받아 셋한다.

abstract class SimpleDataBindingAdapterControl<ITEM : Any> {

    val adapter: SimpleDataBindingAdapter by lazy {
        SimpleDataBindingAdapter {
            onCreateViewHolder()
        }
    }

    /**
     * Use bindViewHolder
     */
    abstract fun SimpleDataBindingAdapterCreateItem.onCreateViewHolder(): SimpleDataBindingViewHolder

    /**
     * adapter add item.
     * @param item Group data class.
     */
    protected abstract fun SimpleDataBindingAdapter.onCreateItems(item: ITEM, viewModel: ViewModel?)

    fun setItems(item: ITEM, viewModel: ViewModel?) {
        adapter.onCreateItems(item, viewModel)
    }
}

결국 onCreateViewHolder와 onCreateItems 2개의 정의로 초기화부터 Adapter 사용이 가능하다.

createDataBindingHolder는 중간에 extension을 구현해뒀기에 레이아웃 만 넘기면 초기화가 끝난다.

item은 직접 forEach를 통해 정의해야 한다.

class SimpleAdapterDataBindingSampleAdapterControl :
    SimpleDataBindingAdapterControl<AdapterDataBindingItemGroup>() {

    /**
     * ViewHolder 정의
     */
    override fun SimpleDataBindingAdapterCreateItem.onCreateViewHolder(): SimpleDataBindingViewHolder {
        return when (viewType) {
            else -> createDataBindingHolder(R.layout.item_data_binding_text_view)
        }
    }


    /**
     * add Item and ViewModel
     */
    override fun SimpleDataBindingAdapter.onCreateItems(
        item: AdapterDataBindingItemGroup,
        viewModel: ViewModel?
    ) {
        val startPosition = getItemSize()
        var newItem = 0
        // AddItem.
        item.itemList.forEach {
            newItem++
            addItem(0, it, viewModel)
        }
        notifyItemRangeInserted(startPosition, newItem)
    }
}

layout은 아래와 같이 정의한다.

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="item"
            type="app.thdev.ui.databinding.AdapterDataBindingItem" />

        <variable
            name="viewModel"
            type="app.thdev.ui.databinding.SimpleAdapterDataBindingSampleViewModel" />
    </data>
</layout>


마무리

AirBnb Epoxy 보다 불편한 부분이 View를 초기화 시켜야 하고, data 정의도 해야 한다는 부분이다. 좀 더 쉽게 만들기 위해서 고민할 부분이 생길 듯하다.

일단은 여기까지 해서 아래와 같이 DataBinding 샘플을 사용하는 게 가능하다.

dependencies {
    implementation "tech.thdev.recyclerview:simple-adapter:$lastVersion"
    implementation "tech.thdev.recyclerview:simple-adapter-databinding:$lastVersion"
}


관련 코드

SimpleAdapter - GitHub



About Taehwan

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

Comments