Android - DataBinding 사용 방법

코틀린 익스텐션으로 findViewById를 사용하지 않아도 쉽게 레이아웃 파일에 선언한 id를 쉽게 가져올 수 있었습니다. 하지만 레이아웃에서 선언한 View가 코틀린 파일의 객체를 읽고 데이터를 출력해주지 않습니다. 코틀린에서 직접 데이터를 View에 설정해줘야 합니다.

DataBinding은 코틀린에서 레이아웃을, 레이아웃에서 코틀린의 데이터를 직접 참조하는 라이브러리입니다. 코틀린에서 레이아웃 파일에 의존적인 부분이 많이 사라지고 MVVM 등의 패턴과 함께 사용됩니다.

One-way Binding과 Two-way Binding

DataBinding에서 One-way bindingTwo-way binding라는 용어가 있습니다.

One-way binding은 데이터의 흐름이 일방향을 말합니다. 만약 Youtube 리스트를 서버에서 받아와서 화면에 보여준다면 데이터의 흐름은 Code -> View으로 일방향입니다.

Two-way binding이라는 용어는 데이터 흐름이 양방향인데요. 위의 예제에서, 사용자가 'android'를 검색하면 android 관련 Youtube 리스트만 보여주도록 구현해야 한다면 양방향 바인딩으로 구현할 수 있습니다. 데이터 흐름은 Code -> View, View -> Code로 양방향입니다.

프로젝트 생성

이전 글 AndroidX RecyclerView 구현(kotlin)에서 만든 프로젝트를 DataBinding을 사용하여 리팩토링을 해보겠습니다.

프로젝트는 GitHub에서 다운받을 수 있습니다.

Gradle

의존성에 따로 추가할 내용은 없습니다. 하지만 App Gradle의 Android 태그 아래에 DataBinding 및 Java8을 사용하도록 옵션을 추가해야 합니다.

android {
    compileSdkVersion 28
    .....
    dataBinding {
        enabled = true
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}

RecyclerView

먼저 RecyclerView만 DataBinding으로 리팩토링하겠습니다. 먼저 xml파일의 형식을 조금 변경해야 합니다. DataBinding에서 사용되는 레이아웃 파일들의 Root 태그는 <layout/>이 되어야 합니다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" ...>
  ....
</layout>

그리고 코틀린의 어떤 객체를 참조하여 데이터를 출력할 지 <data>를 정의해줘야 합니다. youtubeItem의 경우 YoutubeItem 객체를 참조하겠다는 내용입니다.

<data>
    <variable
        name="youtubeItem"
        type="com.codechacha.sample.data.YoutubeItem"/>
</data>

데이터를 선언했으면, @{youtubeItem.title}처럼 데이터를 View에 참조하도록 연결할 수 있습니다.

<TextView
    ...
    android:textSize="20sp"
    android:text="@{youtubeItem.title}"

리스너의 경우 @{youtubeItem::onClickListener}처럼 설정이 가능합니다. 대신 YoutubeItem에 onClickListener 함수를 생성해야 합니다.

<androidx.constraintlayout.widget.ConstraintLayout
    ...
    android:onClick="@{youtubeItem::onClickListener}">

수정된 list_item.xml 파일은 다음과 같습니다.

/res/layout/list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<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="youtubeItem"
                type="com.codechacha.sample.data.YoutubeItem"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="15dp"
            android:paddingBottom="15dp"
            android:background="?android:attr/selectableItemBackground"
            android:onClick="@{youtubeItem::onClickListener}">

        <ImageView
                android:id="@+id/thumbnail"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                android:adjustViewBounds="true"/>

        <TextView
                android:id="@+id/title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="20sp"
                android:text="@{youtubeItem.title}"
                app:layout_constraintTop_toBottomOf="@+id/thumbnail"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

그리고 루트 패키지에 adapters, data폴더를 생성해주세요. 기존의 YoutubeItem.ktdata폴더로 옴겨주시고 RecyclerAdapter.ktadapters폴더로 옴겨주세요. BindingAdapter.kt는 나중에 구현할 파일인데 미리 빈파일만 만들어주세요.

위의 작업이 끝난 후, 프로젝트 구조는 다음과 같습니다.

android databinding

OnClickListener

리스너를 코틀린에서 직접 설정하지 않고 레이아웃 파일에서 설정하였습니다. YoutubeItem의 onClickListener로 설정했기 때문에 구현을 해줘야 합니다. xml에서 직접 리스너로 onClickListener를 설정하였기 때문에 View가 클릭이 되면 저 함수가 호출되면서 토스트가 발생합니다.

YoutubeItem.kt

class YoutubeItem(val image: Drawable, val title: String) {
    fun onClickListener(view: View) {
        Toast.makeText(view.context, "Clicked: $title", Toast.LENGTH_SHORT).show()
    }
}

RecyclerAdapter

레이아웃 파일에서 RecyclerView가 어떤 데이터를 참조할지 설정하였고, 이제 코틀린에서 DataBinding관련 설정을 하면 됩니다.

이전에는 RecyclerAdapter.ktonCreateViewHolder에서 ViewHolder를 생성할 때 인자로 View를 넘겨줬습니다. 지금은 ListItemBinding 객체를 생성해서 넘겨주도록 변경하였습니다.

ListItemBinding는 DataBinding에서 자동으로 생성해주는 클래스인데요. 기본적으로 Binding 클래스 이름은 레이아웃 파일의 이름을 파스칼 표기법(Pascal Case: 합성어의 첫 글자를 대문자로 표기)으로 변경하고 Binding을 접미사로 붙여줍니다. 따라서 ListItemBindinglist_item.xml에서 underbar 제거 및 파스칼표기법으로 변경하고 마지막에 Binding을 붙여 생성되었습니다.

ListItemBinding의 역할은 특정 View에 코틀린 코드와 레이아웃을 바인딩하며, View에 접근할 수 있는 객체입니다.

이전 코드와 비교해보면 xml을 inflate하는 것은 동일하지만, DataBindingUtil을 사용하여 ListItemBinding으로 넘겨주는 것이 차이점입니다.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
        ViewHolder {
    return ViewHolder(
        DataBindingUtil.inflate(
            LayoutInflater.from(parent.context), R.layout.list_item, parent, false
        )
    )
}

그럼 ViewHolder의 인자도 ListItemBinding로 변경해야 합니다. list_item.xml에서 youtubeItem에 실제 데이터를 입력해줘야 합니다. ListItemBinding으로 View에 접근할 수 있다고 했었는데요. binding.youtubeItem처럼 접근할 수 있고 여기에 데이터를 입력해주면 View 내에서 저 값을 참조하여 출력을 합니다.

이전 코드와 비교해보면, Listener를 설정하는 코드는 제거하였는데요. list_item.xml에서 직접 listener를 설정해주었기 때문에 삭제했습니다.

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val item = items[position]
    holder.apply {
        bind(item)
        itemView.tag = item
    }
}

class ViewHolder(
    private val binding: ListItemBinding
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item: YoutubeItem) {
        binding.apply {
            youtubeItem = item
        }
    }
}

수정된 RecyclerAdapter.kt는 다음과 같습니다.

/adapter/RecyclerAdapter.kt

class RecyclerAdapter(private val items: ArrayList<YoutubeItem>) :
    RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {

    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = items[position]
        holder.apply {
            bind(item)
            itemView.tag = item
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
            ViewHolder {
        return ViewHolder(
            DataBindingUtil.inflate(
                LayoutInflater.from(parent.context), R.layout.list_item, parent, false
            )
        )
    }

    class ViewHolder(
        private val binding: ListItemBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: YoutubeItem) {
            binding.apply {
                youtubeItem = item
            }
        }
    }
}

이제 실행해보세요. 타이틀은 보이지만 이미지가 보이지 않을 거에요.

android databinding

왜냐면 위에서 list_item.xml수정할 때 이미지에 대한 참조를 설정하지 않았거든요. TextView는 @{youtubeItem.title}로, YoutubeItem의 title 객체를 참조하도록 하였는데요. ImageView는 이렇게 설정하기 힘들었습니다. title은 문자열인 반면에 image는 Drawable로 사용하고 있거든요. ImageView에 resourceId를 입력하는 속성은 있지만 Drawable을 입력하는 속성은 없어서 설정할 수 없었습니다.

<TextView
    ...
    android:text="@{youtubeItem.title}"

이런 이유로, ImageView에서 Drawable을 받는 속성을 만들어주어야 합니다. 아까 /adapters/BindingAdapter.kt를 만들었는데요. 여기에 다음 코드를 추가해주세요.

@BindingAdapter("imageDrawable")
fun bindImageFromRes(view: ImageView, drawable: Drawable?) {
    view.setImageDrawable(drawable)
}

이 코드는 ImageView에서 app:imageDrawable 속성을 사용할 수 있게 합니다. 함수 이름은 중요하지 않고 @BindingAdapter라는 annotation의 인자로 설정된 이름이 중요합니다. 이 이름으로 ImageView에서 사용할 수 있습니다. 함수의 인자들도 중요한데요. view는 ImageView를 의미하고, drawable은 YoutubeItem의 image를 말합니다.

ImageViewapp:imageDrawable속성을 추가하고 @{youtubeItem.image}를 넣어주세요. 그러면 위에서 구현한 BindingAdapter에서 이 객체를 읽고 ImageView에 설정해줍니다.

<ImageView
    ...
    app:imageDrawable="@{youtubeItem.image}"

실행해보세요. 이제 이미지도 보입니다.

android databinding

MainActivity

이제 MainActivity도 DataBinding으로 변경해보죠. 먼저 xml을 변경해주세요. 여기서는 코틀린의 데이터를 참조할 것이 없어서 <data>는 선언하지 않았습니다.

/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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">

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity">

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recyclerView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:clipToPadding="false"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                tools:listitem="@layout/list_item"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

MainActivity.kt에서 ActivityMainBinding을 생성하면, 이 binding객체를 통해서 recyclerView를 접근할 수 있습니다. 전체적인 코드는 다음과 같습니다.

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this,
            R.layout.activity_main)

        val list = ArrayList<YoutubeItem>()
        list.add(YoutubeItem(getDrawable(R.drawable.image01)!!, getString(R.string.title01)))
        list.add(YoutubeItem(getDrawable(R.drawable.image02)!!, getString(R.string.title02)))
        list.add(YoutubeItem(getDrawable(R.drawable.image03)!!, getString(R.string.title03)))
        list.add(YoutubeItem(getDrawable(R.drawable.image04)!!, getString(R.string.title04)))
        list.add(YoutubeItem(getDrawable(R.drawable.image05)!!, getString(R.string.title05)))
        list.add(YoutubeItem(getDrawable(R.drawable.image06)!!, getString(R.string.title06)))
        list.add(YoutubeItem(getDrawable(R.drawable.image07)!!, getString(R.string.title07)))
        list.add(YoutubeItem(getDrawable(R.drawable.image08)!!, getString(R.string.title08)))
        list.add(YoutubeItem(getDrawable(R.drawable.image09)!!, getString(R.string.title09)))
        list.add(YoutubeItem(getDrawable(R.drawable.image10)!!, getString(R.string.title10)))

        val adapter = RecyclerAdapter(list)

        binding.recyclerView.adapter = adapter
        binding.recyclerView.addItemDecoration(
            DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
    }
}

실행해보세요. 이전과 동일하게 동작합니다.

정리

DataBinding 라이브러리를 이용하면 데이터가 변할 때 추가 코드 없이 View에 자동으로 반영되도록 할 수 있습니다. 일반적인 방식보다 추가 코드도 들어가고 구조가 복잡하다고 느낄 수 있는데요. View와 구현부의 의존성이 줄어드는 것이 장점입니다. 이런 장점으로 코드의 재활용 또는 테스트 가능한 코드를 짜는데 이전보다 쉬워질 수 있습니다.

참고

Loading script...

Related Posts

codechachaCopyright ©2019 codechacha