이번 포스팅에서는 Jetpack의 Data Binding 라이브러리에 대해 알아보도록 하겠습니다.
구글에 따르면 Data Binding이란 프로그래매틱 방식이 아니고 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합할 수 있는 지원 라이브러리라고합니다. 간단히 말해 코틀린 코드와 xml의 UI 컴포넌트를 연결하는 라이브러리라고 생각하시면 될 것 같습니다.
Data Binding은 2015년 Google I/O 에서 소개된 이래 매년 다음과 같이 수많은 개수가 이루어져 왔습니다.
하지만 결국 가장 큰 특징은 다음과 같이 세가지로 정리할 수 있는데 여기서는 LiveData를 설명하는 강의에서 만들었던 앱을 수정해보면서 각 특징을 살펴보도록 하겠습니다.
우선은 데이터바인딩 활성화를 위해 app레벨의 build.gradle에 다음 코드를 추가합니다.
plugins {
+ id 'kotlin-kapt'
}
android {
- buildFeatures.viewBinding true
+ buildFeatures.dataBinding true
}
다음은 Data binding을 적용할 xml 전체를 layout 태그로 감싸고, data 태그를 써서 onCreate에서 지정한 ViewModel을 전달하여 사용준비를 합니다. 이 때 Show Context Actions -> Convert to data binding 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="viewmodel"
type="com.example.viewmodelexample.MyViewModel" />
</data>
...
</layout>
그리고 MainActivity의 onCreate에서 바인딩을 활성화합니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(binding.root)
+ val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
...
+ binding.lifecycleOwner = this
+ binding.viewmodel = myViewModel
}
기존의 setContentView는 주석처리하고 DataBindingUtil을 통해 setContentView를 수행하도록 합니다.
DataBindingUtil에 의해 ViewDataBinding 객체를 만들고 나면 LiveData를 관측하기 위해 lifecycleOwner를 정해주고, xml의 뷰와 연동할 데이터를 가진 뷰모델을 지정합니다.
Data Binding의 첫번째 특징은 findViewById를 코드에서 제거할 수 있다는 점입니다.
뷰를 참조하기 위해서는 다음과 같이 findViewById 함수를 사용해야 하는데요,
findViewById<TextView>(R.id.sample_text).apply {
text = viewModel.userName
}
기존 앱에서는 다음과 같이 View binding을 사용함으로써 findViewById를 코드에서 삭제하고 LiveData를 Observe하고 있었습니다.
myViewModel.modifiedCounter.observe(this) { counter ->
binding.textView.text = counter
}
하지만 Data Binding을 이용해 텍스트뷰와 liveCounter를 다음과 같이 바인딩하면 위의 코드도 필요없게 됩니다.
<TextView
...
android:text="@{viewmodel.modifiedCounter.toString()}"
/>
두번째 특징은 View의 속성을 커스텀할 수 있는 Custom Binding Adapter입니다. 거두절미하고 예제로 설명하도록 하겠습니다.
위에서 만든 카운터를 표시할 ProgressBar를 하나 만들어줍니다.
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
android:max="100"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_percent="0.3"
app:progressScaled="@{viewmodel.liveCounter}" />
카운터 초기값은 10으로 변경합니다. 여기에 app:progressScaled 속성을 새로 만들고 liveCounter와 바인딩합니다. 그러면 liveCounter의 값이 변할때마다 자동으로 app:progressScaled 속성에 값을 전달하게 됩니다.
다음은 BindingAdapters 파일을 만들고 setProgress 함수를 만들어줍니다.
@BindingAdapter("app:progressScaled")
fun setProgress(progressBar: ProgressBar, counter: Int) {
progressBar.progress = counter
}
그러면 app:progressScaled가 값을 전달받을 때마다 바인딩 어댑터에 의해 자동으로 setProgress가 실행되게 됩니다. 이 때 파라미터로는 값을 전달하고 있는 View와 해당하는 속성값을 전달하면 됩니다.
두개 이상의 속성을 동시에 다루고 싶을 경우 다음과 같이 구성하면 됩니다.
// activity_main.xml
<ProgressBar
android:max="@{100}" />
// BindingAdapters.kt
@BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
fun setProgress(progressBar: ProgressBar, counter: Int, max: Int) {
progressBar.progress = (counter * 2).coerceAtMost(max)
}
보여드린 코드는 app:progressScaled와 android:max 항목을 동시에 관찰하면서 변화가 있을 때 프로그래스바의 증가량을 카운터값의 2배로 하되, max값 이상이 되지 않도록 한다는 내용입니다.
"@{ }"을 사용하는 바인딩은 데이터를 뷰로 보내기만 하는 구조입니다. 하지만 뷰가 데이터를 보내게도 할 수 있는데 이것이 Two-way Data Binding입니다. 사용법을 보여드리기 위해 ViewModel에 변수를 하나 추가하겠습니다.
val hasChecked : MutableLiveData<Boolean> = MutableLiveData<Boolean>(false)
그리고 xml에 체크박스를 하나 추가합니다.
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:checked="@={viewmodel.hasChecked}"
android:text="CheckBox"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button" />
이 체크박스는 hasChecked의 값을 받아 true / false 여부를 표시하게 됩니다. 그런데 체크박스는 텍스트뷰와는 달리 UI에서 true / false 여부를 변경할 수도 있죠. 체크박스에서 실행한 조작이 hasChecked의 값을 변화시키게 하려면 "@={viewmodel.hasChecked}" 처럼 @=를 사용합니다.
UI 조작에 의해 hasChecked가 실제로 변하고 있는지 확인하기 위한 텍스트뷰도 하나 추가해줍니다.
<TextView
android:id="@+id/checkBoxView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@{viewmodel.hasChecked.toString()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/checkBox" />
체크박스를 조작해보면 텍스트뷰의 값도 변하는 것을 확인할 수 있습니다. 이렇게 해서 Data Binding에 대해 알아보았습니다.