# Table of Contents

# Data Binding 등장 배경

다음과 같은 레이아웃 XML 파일이 있다고 가정합시다.

// activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/activity_main_tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:id="@+id/activity_main_tv_nation"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

보통 액티비티에서는 findViewById()를 통해 뷰를 객채화합니다. 그리고 객체화한 뷰의 변수나 메소드로 UI를 수정합니다.

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    lateinit var textViewName: TextView
    lateinit var textViewNation: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        textViewName = findViewById(R.id.activity_main_tv_name)
        textViewNation = findViewById(R.id.activity_main_tv_nation)

        // 데이터
        val person = Person("Paul", "USA")

        // UI 수정
        textViewName.text = person.name
        textViewNation.text = person.nation
    }
}
// MainActivity.kt
package com.yologger.project.data

data class Person(val name: String, val nation: String)

Data Binding을 사용하면 UI 요소와 데이터를 보다 쉽게 결합할 수 있습니다.

# 설정

Data Binding을 사용하려면 설정이 필요합니다.

// 모듈 수준의 build.gradle
android {
    dataBinding {
        enabled = true
    }
}

# 사용법

Data Binding을 사용하려면 레이아웃 XML 파일 전체를 <layout>태그로 감싸줘야합니다.

// activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <LinearLayout
        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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </LinearLayout>
</layout>

앱을 리빌드하면 Binding 클래스가 생성됩니다. 예를 들어 레이아웃 XML 파일이 activity_main.xml이라면 ActivityMainBinding클래스가 생성됩니다.

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        // ..
    }
}

이 클래스의 인스턴스는 DataBindingUtil 클래스를 사용하여 다음과 같이 초기화합니다.

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    }
}

그 다음 <data>태그를 사용하여 변수를 추가합니다.

// activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>
        <variable
            name="person"
            type="com.yologger.project.data.Person" />
    </data>

    <LinearLayout
        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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </LinearLayout>
</layout>

이후 앱을 다시 한번 리빌드합니다. 그러면 ActivityMainBinding클래스에 person이라는 변수가 생성됩니다. 이 변수로 데이터를 전달합니다.

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        val person = Person("Paul", "USA")
        binding.person = person
    }
}

이제 레이아웃 XML 파일에서 전달받은 변수에 접근할 수 있습니다. 변수에 접근할 때는 @{}를 사용합니다.

// activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>
        <variable
            name="person"
            type="com.yologger.project.data.Person" />
    </data>

    <LinearLayout
        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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{person.name}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{person.nation}" />

    </LinearLayout>
</layout>

# ViewModel, LiveData와 함께 사용하기

Data BindingViewModel, LiveData와 함께 사용할 수 있습니다. 간단한 로그인 화면 예제를 살펴보겠습니다.

MainViewModel클래스는 다음과 같습니다.

// MainViewModel.kt
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel: ViewModel() {

    val liveEmail: MutableLiveData<String> = MutableLiveData<String>().apply { value = "" }
    val livePassword: MutableLiveData<String> = MutableLiveData<String>().apply { value = "" }

    fun login() {
        // do something..
    }
}

MainActivity클래스는 다음과 같습니다.

// MainActivity.kt
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.yologger.project.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // setContentView(R.layout.activity_main)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.viewModel = viewModel
    }
}

activity_main.xml은 다음과 같습니다.

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

    <data>
        <variable
            name="viewModel"
            type="com.yologger.project.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textEmailAddress"
            android:text="@={viewModel.liveEmail}"
            android:hint="email"/>

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="textPassword"
            android:text="@={viewModel.livePassword}"
            android:hint="password"/>

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{(view) -> viewModel.login()}"
            android:text="Login" />
    </LinearLayout>
</layout>

@={}양방향 바인딩이라고 합니다. EditText의 값을 수정하면 바인딩된 LiveData값도 변경됩니다. 변경된 값이 다시 EditText의 값도 수정되어야하므로 양방향이라는 표현을 사용합니다.

@{(view) -> viewModel.login()} 형태로 클릭 이벤트도 처리할 수 있습니다.

# BindingAdapter

BindingAdapter를 사용하면 View의 속성을 사용자가 직접 추가할 수 있습니다. 예제를 살펴보겠습니다. 예제는 Switch값에 따라 View의 가시성이 토글됩니다. (보였다가 안보였다가 합니다)

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

    <data>
        <variable
            name="viewModel"
            type="com.yologger.project.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <View
            android:id="@+id/view"
            android:background="#FFA334"
            android:layout_width="200dp"
            android:layout_height="200dp" />

        <Switch
            android:id="@+id/switch1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onCheckedChanged="@{(switch, isChecked) -> viewModel.onCheckedChanged(isChecked)}"
            android:text="Invisible/Visible" />
    </LinearLayout>
</layout>

MainViewModel클래스는 다음과 같습니다.

// MainViewModel.kt
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel: ViewModel() {

    val liveIsVisible: MutableLiveData<Boolean> = MutableLiveData<Boolean>().apply { value = false }

    fun onCheckedChanged(isChecked: Boolean) {
        liveIsVisible.value = isChecked
    }
}

MainActivity클래스는 다음과 같습니다.

// MainViewModel.kt
import com.yologger.project.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // setContentView(R.layout.activity_main)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.viewModel = viewModel
    }
}

MainViewModelBoolean타입의 liveIsVisible변수를 가지고 있습니다. 이 변수는 true, false 값만 가질 수 있습니다. 그러나 View의 가시성은 true, false가 아니라 다음과 같이 제어해야합니다.

val view: View

view.visibility = View.VISIBLE
view.visibility = View.INVISIBLE

따라서 이 중간을 이어주는 무엇인가가 필요한데 이 것이 BindingAdapter 입니다. BindingAdapter는 다음과 같이 정의합니다.

// ViewBindingAdapter.kt
object ViewBindingAdapter {

    @BindingAdapter("android:isVisible")
    @JvmStatic
    fun setVisibility(view: View, isVisible: Boolean) {
        view.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
    }
}

이제 레이아웃 XML 파일에서 마치 커스텀 속성처럼 android:isVisible을 사용할 수 있습니다.

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

    <data>
        <variable
            name="viewModel"
            type="com.yologger.project.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <View
            android:id="@+id/view"
            android:background="#FFA334"
            android:layout_width="200dp"
            android:isVisible="@{viewModel.liveIsVisible}"
            android:layout_height="200dp" />

        <Switch
            android:id="@+id/switch1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onCheckedChanged="@{(switch, isChecked) -> viewModel.onCheckedChanged(isChecked)}"
            android:text="Invisible/Visible" />
    </LinearLayout>
</layout>