안드로이드 공부를 하면서 databinding을 많이 사용해왔으나 databinding을 지양하는 것이 좋다는 말을 주변에서 듣고 무심코 써왔던 databinding에 대해 내가 자세히 알고 있지 못하다고 생각하게 되어 이 글을 쓰게 되었다.
DataBinding이란
Data Binding은 안드로이드 아키텍처 구성요소 중 하나로 UI 구성요소와 앱의 데이터 소스를 선언적으로 연결할 수 있게 하는 라이브러리이다.
DataBinding 장점
레이아웃 파일에서 UI 구성요소를 앱 데이터와 연결하면 액티비티에서 UI 프레임워크의 호출을 줄일 수 있어서 코드가 간결해지고 유지관리가 쉬워진다는 장점이 있다. 또한 앱 성능이 향상되며, 메모리 누수 및 Null Pointer 예외를 방지할 수 있다.
1. 가독성 향상
- MVVM 패턴 구현을 단순화함
- 보일러플레이트를 줄여줌
2. 타입 안전성 향상
- 컴파일 타임에 데이터 타입을 체크함으로써 런타임 에러가 줄어듦
3. 성능 향상
- findViewById() 의 호출 수를 줄임
- UI 업데이트에 필요한 코드를 줄임
4. LiveData와의 손쉬운 통합
- LiveData와 긴밀하게 통합되어 반응형 UI 구현 단순화
5. 양방향 바인딩 지원
- UI와 데이터 모델 간의 양방향 바인딩이 가능하여 데이터를 실시간으로 쉽게 업데이트 할 수 있음
DataBindig 적용하기
레이아웃파일
Data Binding 라이브러리는 레이아웃의 뷰를 데이터 객체와 결합하는데 필요한 클래스를 자동으로 생성한다.
레이아웃 파일 작성
Data Binding을 사용하는 레이아웃 파일은 layout이라는 루트 태그로 시작하고 그 안에 해당 레이아웃에서 사용할 데이터를 data 태그를 통해 명시합니다. 그런 다음 레이아웃을 구성할 뷰들을 배치한다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
앱의 데이터는 @{} 구문을 통해 뷰의 특정 속성에 지정된다.
표현식
@{} 표현식에는 this, super, new, 명시적 제네릭 호출을 제외한 여러 연산자와 키워드를 사용할 수 있다.
<TextView
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}" />
또한 표현식에는 Null 병합 연산자를 사용할 수 있다.
<TextView
android:text="@{user.displayName ?? user.lastName}" />
Null 병합 연산자
왼쪽 피연산자가 NULL이 아니면 왼쪽 피연산자를 선택, NULL이면 오른쪽 피연산자를 선택
표현식은 ID를 통해 레이아웃의 다른 뷰를 참조할 수도 있다.
<EditText
android:id="@+id/example_text"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
<TextView
android:id="@+id/example_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{exampleText.text}"/>
데이터 결합
Data Binding도 각 레이아웃 파일의 바인딩 클래스를 생성하는데, 이 클래스는 View Binding처럼 레이아웃 파일 이름을 파스칼 표기법으로 변환한 뒤, Binding이라는 접미사를 추가한 이름을 갖는다.
이 바인딩 클래스에는 레이아웃 속성(데이터 변수 등)에서부터 레이아웃 뷰까지 모든 바인딩을 갖고 있으며, 어떻게 바인딩 표현식의 값을 할당할지도 알고 있다.
바인딩 객체 생성 방법
권장되는 결합 생성 방법은 레이아웃이 inflating을 하는 동안에 생성하는 것이다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User("Test", "User")
}
다른 방법으로는 LayoutInflater를 이용하는 것이 있다.
val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())
Fragment, ListView, RecyclerView 어댑터 내에서 Data Binding을 사용한다면 inflate() 메서드를 사용하여 바인딩 객체를 생성할 수 있다.
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
이벤트 처리
Data Binding을 사용하면 뷰에서 전달되는 표현식 처리 이벤트를 작성할 수 있습니다. 이벤트 속성의 이름은 대부분 리스너 메서드의 이름에 따라 결정된다.
메서드 참조
Data Binding에서 표현식이 메서드 참조로 계산되면 리스너에서 메서드 참조 및 소유자 객체를 래핑하고, 타겟 뷰에서 이 리스너를 설정한다.
이벤트는 android:onClick이 액티비티의 메서드에 할당되는 방식과 유사하게 핸들러 메서드에 직접 결합될 수 있는데요, Data Binding의 메서드 참조는 표현식이 컴파일 타임에 처리된다는 장점이 있다.
class MyHandlers {
fun onClickFriend(view: View) { ... }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.MyHandlers" />
<variable name="user" type="com.example.User" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}" />
</LinearLayout>
</layout>
메서드의 매개변수는 이벤트 리스너의 매개변수와 일치해야 한다.
메서드 참조이 리스너 결합과 다른 점은 실제 리스너 구현이 이벤트가 트리거될 때가 아닌, 데이터가 결합될 때 생성된다는 것이다. 따라서 이벤트가 발생할 때 표현식을 계산하려면 리스너 결합을 사용해야 한다.
리스너 결합
리스너 결합은 메서드 참조와 달리 이벤트가 발생할 때 실행되는 결합 표현식이며, 메서드와 이벤트 리스너의 반환 값만 일치하면 된다.
class Presenter {
fun onSaveClick(task: Task){}
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>
표현식에 콜백을 사용하면 데이터 결합은 필요한 리스너를 자동으로 생성하여 이벤트에 등록한다.
리스너 결합에서는 모든 매개변수를 무시하거나, 모든 매개변수의 이름을 지정하여 매개변수를 선택할 수 있다. 매개변수 이름을 지정하면 표현식에 매개변수를 사용할 수 있다.
<Button
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}" />
class Presenter {
fun onSaveClick(view: View, task: Task){}
}
Databinding을 사용하면 코드가 단순해지고, 메모리 누수가 방지된다는 장점이 있다.
import, variable, include
- import를 사용하면 레이아웃 파일 내에서 클래스를 참조할 수 있습니다.
- variable을 사용하면 결합 표현식에 사용할 수 있는 속성을 설명할 수 있습니다.
- include를 사용하면 앱 전체에서 복잡한 레이아웃을 재사용할 수 있습니다.
import
data 태그 내에 0개 이상의 import 요소를 사용할 수 있습니다.
<data>
<import type="android.view.View" />
</data>
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}" />
이처럼 import를 통해 클래스를 가져오면 표현식에서 해당 클래스를 참조할 수 있습니다.
별칭을 사용하여 클래스 이름 충돌을 해결할 수 있습니다.
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
가져온 클래스는 변수 및 표현식에서 유형 참조로 사용할 수도 있습니다
<data>
<import type="com.example.User" />
<import type="java.util.List" />
<variable name="user" type="User" />
<variable name="userList" type="List<User>" />
</data>
형변환 또한 가능합니다.
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
variable
data 태그 내에 여러 variable 요소를 사용할 수 있습니다.
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
include
속성에 앱 네임스페이스 및 변수 이름을 사용함으로써 포함하는 레이아웃에서 포함된 레이아웃의 결합으로 변수를 전달할 수 있습니다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/name"
bind:user="@{user}"/>
<include
layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>
식별 가능한 데이터 객체와 Data Binding
Data Binding을 통해 객체, 필드 또는 컬렉션을 식별 가능하게 만들 수 있다.
식별 가능성
객체가 데이터 변경에 관해 다른 객체에 알릴 수 있는 기능
Data Binding을 사용하면 데이터 변경 시 리스너에 알리는 기능을 데이터 객체에 제공할 수 있는데, 이를 통해 UI를 자동으로 업데이트할 수 있다.
식별 가능한 필드
필드는 일반 Observable 클래스 및 Observable Primitive 클래스를 사용하여 식별 가능하게 만들 수 있다.
식별 가능한 필드는 단일 필드가 있는 독립적 객체입니다. Primitive 버전은 액세스 작업 중에 박싱 및 언박싱을 방지해야 하기 때문에 읽기 전용 속성으로 만들어야 한다.
class User {
val firstName = ObservableField<String>()
val lastName = ObservableField<String>()
val age = ObservableInt()
}
식별 가능한 컬렉션
식별 가능한 컬렉션은 키를 통해 접근할 수 있다.
키가 참조 유형일 때는 ObservableArrayMap 클래스가 유용하다.
ObservableArrayMap<String, Any>().apply {
put("firstName", "Google")
put("lastName", "Inc.")
put("age", 17)
}
<data>
<import type="android.databinding.ObservableMap" />
<variable name="user" type="ObservableMap<String, Object>" />
</data>
<TextView
android:text="@{user['lastName']}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
키가 정수일 때는 ObsevableArrayList 클래스가 유용하다.
ObservableArrayList<Any>().apply {
add("Google")
add("Inc.")
add(17)
}
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
식별 가능한 객체
Observable 인터페이스를 구현하면 식별 가능한 객체의 변경에 관한 알림을 받는 리스너를 등록할 수 있다.
Data Binding 라이브러리는 리스너 등록 메커니즘을 구현하는 BaseObservable 클래스를 제공한다.
class User : BaseObservable() {
@get:Bindable
var firstName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.firstName)
}
@get:Bindable
var lastName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.lastName)
}
}
Data Binding은 데이터 바인딩에 사용된 리소스의 ID를 포함하는 모듈 패키지에 이름이 BR인 클래스를 생성한다. Bindable 주석은 컴파일하는 동안 BR 클래스 파일에 항목을 생성한다.
바인딩 어댑터
바인딩 어댑터는 적절한 프레임워크를 호출하여 값을 설정하는 작업을 담당한다.
속성 값 설정
결합된 값이 변경될 때마다 생성된 결합 클래스는 결합 표현식을 사용하여 속성 값을 설정해야 한다.
자동 메서드 선택
라이브러리는 속성의 이름과 타입을 토대로 관련된 setter 메서드를 찾습니다. 예를 들어 android:text="@{user.name}" 표현식이 있는 경우 라이브러리는 user.getName()에서 반환한 타입을 허용하는 setText(arg) 메서드를 찾는다.
맞춤 메서드 이름 지정
일부 속성에는 이름이 일치하지 않는 setter가 있다. 이러한 상황에서 속성은 BindingMethods 주석을 사용하여 setter와 연결될 수도 있습니다. 주석은 클래스와 함께 사용되며 이름이 바뀐 각 메서드에 하나씩 여러 BindingMethod 주석을 포함할 수 있다.
@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])
위 예에서 android:tint 속성은 setTint() 메서드가 아닌 setImageTintList(ColorStateList) 메서드와 연결된다.
맞춤 로직 제공
일부 속성에는 맞춤 결합 로직이 필요하다. BindingAdapter 주석이 있는 정적 바인딩 어댑터 메서드를 사용하면 속성의 setter가 호출되는 방식을 맞춤설정할 수 있다.
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
바인딩 어댑터 메서드에서 첫 번째 매개변수는 속성과 연결된 뷰의 타입을 결정한다. 두 번째 매개변수는 지정된 속성의 결합 표현식에서 허용되는 타입을 결정한다.
개발자가 정의하는 바인딩 어댑터는 충돌이 발생하면 Android 프레임워크에서 제공하는 기본 어댑터보다 우선 적용된다.
여러 속성을 받는 어댑터도 있을 수 있다.
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
속성이 하나라도 설정될 때 어댑터가 호출되도록 하려면 다음 예에서와 같이 어댑터의 선택적 requireAll 플래그를 false로 설정할 수 있다.
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
바인딩 어댑터 메서드는 선택적으로 핸들러의 이전 값을 사용할 수 있다. 이전 값과 새 값을 사용하는 메서드는 아래 예에서와 같이 속성의 모든 이전 값을 먼저 선언한 후 새 값을 선언해야 한다.
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
if (oldPadding != newPadding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
}
이벤트 핸들러는 다음 예에서와 같이 하나의 추상 메서드가 있는 인터페이스 또는 추상 클래스에서만 사용할 수 있다.
@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
view: View,
oldValue: View.OnLayoutChangeListener?,
newValue: View.OnLayoutChangeListener?
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue)
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue)
}
}
}
그렇다면 databinding을 지양해야하는 이유는 무엇일까?
1. 강결합된 코드베이스
- 디버깅을 더 어렵게 만듦
- 코드베이스가 강결합이 되어, 기본 클래스를 사용할 수 없음
2. 관심사의 분리
- UI와 데이터를 조작하는 코드가 섞여있기 때문에 코드를 읽고 이해하기 어려움
- 앱이 복잡한 경우 코드 디버깅 및 유지보수가 더 어려워질 수 있음
3. 테스트 및 디버깅의 어려움
- 생성된 코드이기 때문에, 일반 코드에 비해 디버깅하기가 어려움
- 생성된 코드는 프로젝트에서 보이지 않기 때문에, 발생하는 문제를 추적하기 어려움
- 데이터바인딩이 포함된 코드를 단위테스트하는 것은 상대적으로 어려움
데이터바인딩의 현 주소
1. kapt에 의존
- 데이터바인딩은 kapt에 의존적임
- kapt는 유지 관리 모드에 있음
- 더이상 새로운 기능을 구현할 계획이 없음
- ref. kapt compiler plugin
- kapt 없이 ksp를 단독으로 사용하는 경우에만 ksp의 성능 이점을 누릴 수 있음
- 데이터바인딩을 사용하는 한 kapt를 사용해야 하므로, 데이터바인딩이 장애물이 됨
2. 유지 관리 모드
- 구글에서 데이터바인딩도 유지 관리 모드라는 것을 공개함
- Databinding is in maintenance mode as well.
We don't plan to support KSP nor recommend data binding usage at this stage since compose is our recommended UI solution. - 데이터바인딩에 KSP를 지원한다거나 데이터바인딩 사용을 권장할 계획이 없음
- 언젠가 kapt가 deprecated되면 데이터바인딩도 동작하지 않게 될 가능성이 있음
'Android' 카테고리의 다른 글
[Android] View Binding (1) | 2023.08.30 |
---|---|
[Android] AAC (Android Architecture Components) (0) | 2023.08.30 |
[Android] Multi-ViewType RecyclerView (1) | 2023.08.09 |
[Android] RecyclerView (1) | 2023.08.03 |
[Android] Fragment 생명주기 (1) | 2023.04.20 |