서론
해당 포스트는 Android Jetpack 중 하나인 Data Binding 에 대해서 정리한 것입니다.
Databinding
DataBinding 은 선언적 UI 방식을 지원하기 위한 라이브러리로 Data와 View를 결합을 시킵니다.
DataBinding 의 장점은 아래와 같습니다.
- data 가 바뀌면 자동으로 View 를 변경하게 할 수 있다.
- xml 리소스만 보고도 View 에 어떤 데이터가 들어가는지 파악이 가능하다.
- 코드 가독성이 좋아지고, 상대적으로 코드량이 줄어든다.
Data binding는 주로 Android MVVM 패턴을 구현하기 위해 사용이 됩니다. 참고로 MVVM 패턴을 구현할 때에는 Data binding외에 아래 라이브러리과 같이 사용하게 됩니다.
Data를 읽어서 View 를 업데이트하는 고전적인 방식은 아래와 같이 findViewByID() api를 사용하여 View를 접근한 다음 set api를 사용하여 View에 data를 set를 하게됩니다.
TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());
하지만 Data binding 을 사용함으로써 자바/코틀린 코드 없이 xml 파일에 직접 data 를 할당함으로써 view 를 업데이트하게 됩니다.
<TextView
android:text="@{viewmodel.userName}" />
참고로 xml 에 할당식을 작성할 때에는 @{}
를 사용합니다.
Data Binding 사용법
1. Layout 파일 수정
Data와 바인딩할 기존 layout 파일을 아래와 같이 수정해야 합니다.
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewmodel"
type="com.origogi.gallery.vm.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MainActivity">
<TextView
android:id="@+id/counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="15dp"
android:text="@{`Image Count : ` + viewmodel.counter}"
app:layout_constraintBottom_toTopOf="@id/image_title_list"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="count : 5" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/image_title_list"
android:layout_width="match_parent"
android:layout_height="0dp"
bindItem="@{viewmodel.imageDataList}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/counter" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
- 먼저 root를
<layout>
으로 감쌉니다. - Binding 할 data를 을 사용하여 선언합니다.
- 할당식
@{}
를 사용해서 View와 Data 를 연결합니다.
2. Data 객체 선언
위의 XML에 해당하는 Data 클래스를 선언합니다.
class MyViewModel : ViewModel() {
private val imageDataList: MutableLiveData<List<ImageData>> =
MutableLiveData<List<ImageData>>()
private val counter: MutableLiveData<Int> = MutableLiveData<Int>()
fun getImageDataList() = imageDataList as LiveData<List<ImageData>>
fun getCounter() = counter as LiveData<Int>
}
위 예제에서는 Data를 LiveData 를 사용했습니다.
3. View와 Data 를 연결
액티비티나 프래그먼트에 아래와 같이 데이터 바인딩을 시도합니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
val viewModel = ViewModelProvider(this)[MyViewModel::class.java]
binding.viewmodel = viewModel
binding.lifecycleOwner = this
}
여기서 ActivityMainBinding
은 컴파일러를 통해 생성된 클래스입니다. 이름은 xml 파일명 기준으로 생성이 되며 여기서 사용된 xml이름은 activity_main.xml
입니다.
binding.viewmodel
에서 viewmodel
은 activity_main.xml
안에 <data>
안에 name
기준으로 생성이 됩니다. binding.lifecycleOwner
같은 경우 Data가 LiveData 인 경우 추가적으로 등록해야 합니다.
4. BindingAdapter 설정
BindingAdapter 는 Data가 변경될 경우 안드로이드 프래임워크에서 자동으로 실행하는 API입니다.
BindingAdapter 은 위 예제에서 아래와 같이 android:text
와 bindItem
이 있습니다.
<TextView
...
android:text="@{`Image Count : ` + viewmodel.counter}"
/>
<androidx.recyclerview.widget.RecyclerView
...
bindItem="@{viewmodel.imageDataList}"
/>
여기서 android:text
는 Pre-define 된 BindingAdapter이고 bindItem
은 커스텀으로 추가한 BindingAdapter 입니다.
android:text
은 아래와 같이 TextViewBindingAdapter
에 정의가 되어 있습니다.
public class TextViewBindingAdapter {
...
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
}
bindItem
은 안드로이드에서 기본적으로 제공하지 않는 BindingAdapter 로 아래와 같이 추가할수 있으며 @BindingAdapter 어노테이션을 이용하여 이름을 일치시킵니다.
@BindingAdapter("bindItem")
fun bindItem(recyclerView: RecyclerView, items: LiveData<List<ImageData>>) {
val adapter = recyclerView.adapter
if (adapter is ImageDataAdapter) {
items.value?.let {
println("bind item ${it.size}")
adapter.update(it)
}
}
}
Appendix. Data object 교체
일반적으로 View와 Data Object를 binding을 하고 Data Object 안의 맴버 변수를 업데이트 함으로써 View를 업데이트하지만 Data Object 자체를 변경해야 하는 case가 있을 수 있습니다.
예를 들어 RecyclerView의 ViewHolder를 업데이트할 때입니다.
class ImageDataAdapter(private val context: Context) :
RecyclerView.Adapter<ImageDataAdapter.ViewHolder>() {
private val imageDataList = mutableListOf<ImageData>()
class ViewHolder(private val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(itemData : ImageData) {
binding.item = itemData
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val imageData = imageDataList[position]
holder.bind(imageData)
}
...
}
위의 Data Binding 동작 순서는 다음과 같습니다.
- onCreateViewHolder() 에 ListItemBinding.inflate() 를 이용하여 binding 을 생성합니다. ListItemBinding 은 컴파일러가 생성한 클래스로써 이름은 xml 파일명 기준으로 생성이 되며 여기서 사용된 xml이름은
list_item.xml
입니다. - binding 객체를 ViewHolder가 가지도록 합니다.
- onBindViewHolder() 가 호출 될 때 ViewHolder의 bind() 를 호출합니다.
- binding의 item에 새로운 data를 set을 함으로써 Data binding의 data 자체를 교체합니다.
- 마지막으로 binding.executePendingBindings() 를 실행함으로써 안드로이드 프레임워크에 Data 가 변경되었음을 알립니다.
정리
Data binding 같은 경우 여러가지 장점도 있지만 동작이 매우 복잡하고 러닝 커브가 존재하여 기존의 구조에 적용하기 어려울수도 있으며 앱 빌드시간이 늘어난다는 단점이 있습니다. 따라서 Data binding을 적용할 때 특장점을 잘 파악하고 적용하는 것이 중요합니다.
댓글남기기