-
Notifications
You must be signed in to change notification settings - Fork 0
Data Binding
Android API Level 7 이후부터는 데이터 바인딩을 지원한다. DataBinding이란 xml에 데이터를 바인딩하여 불필요한 코드를 줄이는 방법으로, 보통 MVP or MVVM 패턴을 구현 할 때 사용한다. DataBinding 라이브러리는 프로그래매틱 방식이 아니라 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합할 수 있는 지원 라이브러리이다. 레이아웃은 흔히 UI 프레임워크 메서드를 호출하는 코드가 포함된 활동에서 정의된다. 예를 들어 아래 코드는 findViewById()를 호출하여 TextView 위젯을 찾아 viewModel 변수의 userName 속성에 결합하는 예이다.
findViewById(R.id.sample_text).apply {
text = viewModel.userName
}
하지만 DataBinding 라이브러리를 사용하면 위의 Kotlin 코드를 호출할 필요가 없다.
레이아웃 파일에서 구성요소를 결합하면 활동에서 많은 UI 프레임워크 호출을 삭제할 수 있어 파일이 더욱 단순화되고 유지관리 또한 쉬워지며 앱 성능이 향상되고 메모리 누수 및 null 포인터 예외를 방지할 수 있다.
2.1. DataBinding 설정 build.gradle 상단에 데이터 바인딩 사용을 위한 아래 코드를 추가한다.
apply plugin: "kotlin-kapt"
android {
....
dataBinding {
enabled = true
}
}
xml에 연동할 ViewModel.kt 파일을 아래와 같이 생성한다.
class ViewModel {
val text = ObservableField("")
fun showText(view: View) {
Toast.makeText(view.context, "${text.get()}", Toast.LENGTH_SHORT).show()
}
}
뷰모델 클래스에 작성해둔 함수는 xml에서 호출 할 수 있다. 다만, 접근 제한자가 private면 xml에서 함수를 참조할 수 없다.
데이터 바인딩을 위해서 루트 태그를 layout으로 바꿔 주고 variable 태그를 통해 레이아웃에 연동할 ViewModel을 지정해준다.
ViewModel의 데이터에 접근 할 때에는, @{} 안에 참조 할 데이터를 작성한다. DataBinding을 사용하면 레이아웃에서 간단한 식을 사용할 수 있다. 위 코드의 android:visiblity가 간단한 예다.
DataBinding에서는 android:onClick등의 이벤트가 발생했을 때 수행할 동작을 바인딩 할 수 있다. 함수를 바인딩 할 때에는, @{() -> vm.doSomething()} 같이 람다식을 사용해서 호출한다. 이 때 함수는 이벤트의 파라미터를 받거나 context 를 파라미터로 받아서 사용 할 수 있다.
바인딩을 위해 Activity 파일에 아래의 코드를 작성한다.
val binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.vm = ViewModel()
setContentView는 DataBinding을 하며 써주기 때문에, 작성 할 필요가 없으니 지워준다. 데이터바인딩 클래스는 은 레이아웃 파일의 이름에 따라서 자동으로 카멜 케이스로 변환되고, 그 뒤에 Binding을 붙여서 완성된다. ex) activity_main → ActivityMainBinding binding.vm 과 같은 방법으로 레이아웃의 데이터에 접근한다. binding.vm = ViewModel() 로 뷰모델을 바인딩해준다.
데이터 결합을 사용하면 뷰에서 전달되는 표현식 처리 이벤트를 작성할 수 있다(예: onClick() 메서드). 이벤트 속성 이름은 몇 가지 예외를 제외하고 리스너 메서드의 이름에 따라 결정된다. 예를 들어 View.OnClickListener에는 onClick() 메서드가 있으므로 이 이벤트의 속성은 android:onClick이다. 클릭 이벤트에는 충돌을 방지하기 위해 android:onClick 이외의 다른 속성이 필요한 특수한 이벤트 핸들러가 있다. 개발자는 아래 2개 메커니즘을 사용하여 이벤트를 처리할 수 있다.
- Method reference: 표현식에서 리스너 메서드의 서명과 일치하는 메서드를 참조할 수 있다. 표현식이 메서드 참조로 계산되면 데이터 결합은 리스너에서 메서드 참조 및 소유자 객체를 래핑하고 타겟 뷰에서 이 리스너를 설정한다. 표현식이 null로 계산되면 데이터 결합은 리스너를 생성하지 않고 null 리스너를 설정한다.
- Listener binding: 이벤트가 발생할 때 계산되는 람다 표현식이다. 데이터 결합은 항상 리스너를 생성하여 뷰에서 설정한다. 이벤트가 전달되면 리스너는 람다 표현식을 계산한다.
이벤트는 android:onClick이 활동의 메서드에 할당되는 방식과 유사하게 핸들러 메서드에 직접 결합될 수 있다. View onClick 속성과 비교했을 때 주요 이점은 표현식이 컴파일 타임에 처리되므로 메서드가 없거나 서명이 올바르지 않으면 컴파일 타임 오류가 발생한다는 점이다. Method reference와 Listener binding의 주요 차이점은 실제 리스너 구현이 이벤트가 트리거될 때가 아니라 데이터가 결합될 때 생성된다는 점이다. 이벤트가 발생할 때 표현식을 계산하려면 Listener binding을 사용해야 한다. 핸들러에 이벤트를 할당하려면 호출할 메서드 이름이 될 값을 사용하여 일반 결합 표현식을 사용해야 한다.
class MyHandlers {
fun onClickFriend(view: View) { ... }
}
// 결합 표현식은 다음과 같이 뷰의 클릭 리스너를 onClickFriend() 메서드에 할당할 수 있다.
<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}"/>
Listener binding은 이벤트가 발생할 때 실행되는 결합 표현식이다. Listener binding은 Method reference와 비슷하다. 하지만 Listener binding을 사용하면 임의의 데이터 결합 표현식을 실행할 수 있다. 이 기능은 Gradle 버전 2.0 이상을 위한 Android Gradle 플러그인으로 사용할 수 있다. Methon reference에서 메서드의 매개변수는 이벤트 리스너의 매개변수와 일치해야 한다. Listener binding에서는 반환 값만 리스너의 예상 반환 값과 일치하면 된다.(void가 예상되지 않는 한).
class Presenter {
fun onSaveClick(task: Task){}
}
// 다음과 같이 클릭 이벤트를 onSaveClick() 메서드에 결합할 수 있다.
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
표현식에 콜백을 사용하면 Data Binding은 필요한 리스너를 자동으로 생성하여 이벤트에 등록한다. 뷰에서 이벤트가 발생하면 Data Binding은 주어진 표현식을 계산한다. 일반 결합 표현식에서와 같이 이러한 리스너 표현식이 계산되는 동안 계속 Data Binding의 null 및 스레드 안전성이 확보된다. 위의 예에서는 onClick(View)에 전달되는 view 매개변수가 정의되지 않았다. Listener binding에서는 두 가지 방식(모든 매개변수를 무시, 모든 매개변수의 이름을 지정)으로 리스너 매개변수를 선택할 수 있다. 매개변수 이름 지정을 선택하면 표현식에 매개변수를 사용할 수 있습니다. 예를 들어 위의 표현식을 다음과 같이 작성할 수 있다.
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
class Presenter {
fun onSaveClick(view: View, task: Task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
아래와 같이 둘 이상의 매개변수와 함께 람다 표현식을 사용할 수도 있다.
class Presenter {
fun onCompletedChanged(task: Task, completed: Boolean){}
}
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
수신 중인 이벤트가 void가 아닌 유형의 값을 반환하면 표현식도 같은 유형의 값을 반환해야 한다. 예를 들어 '길게 클릭' 이벤트를 수신 대기하려면 표현식에서 Boolean을 반환해야 한다.
class Presenter {
fun onLongClick(view: View, task: Task): Boolean { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
null 객체로 인해 표현식을 계산할 수 없으면 데이터 결합은 각기 해당하는 유형의 기본값을 반환한다. 예를 들어 참조 유형은 null을, int는 0을, boolean은 false를 기본값으로 반환한다. 조건자와 함께 표현식(예: 삼항)을 사용해야 한다면 void를 기호로 사용할 수 있다.
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
Data Binding 라이브러리는 Imports, variables, and includes과 같은 기능을 제공한다. Imports를 사용하면 레이아웃 파일 내에서 클래스를 쉽게 참조할 수 있다. variables를 사용하면 결합 표현식에 사용할 수 있는 속성을 설명할 수 있다. includes을 사용하면 앱 전체에서 복잡한 레이아웃을 재사용할 수 있다.
Imports를 사용하면 관리형 코드에서와 같이 레이아웃 파일 내에서 클래스를 쉽게 참조할 수 있다. 0개 이상의 import 요소를 data 요소 내에서 사용할 수 있다. 아래 코드 예는 View 클래스를 레이아웃 파일로 가져온다.
// View 클래스를 가져오면 결합 표현식에서 참조할 수 있다.
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
클래스 이름 충돌이 발생하면 클래스 중 하나의 이름을 별칭으로 바꿀 수 있다. 아래 코드 예는 com.example.real.estate 패키지의 View 클래스 이름을 Vista로 변경한다.
이제 Vista를 사용하여 com.example.real.estate.View를 참조할 수 있다. 그리고 레이아웃 파일 내에서 android.view.View를 참조하는 데 View를 사용할 수 있다.
가져온 유형은 변수 및 표현식에서 유형 참조로 사용할 수 있다.
아래 예는 변수의 유형으로 사용되는 User 및 List를 보여준다.
주의: Android 스튜디오에서는 아직 가져오기를 처리하지 못하므로 가져온 변수의 자동 완성이 IDE에서 작동하지 않을 수 있다. 그러나 앱은 여전히 컴파일된다. 그리고 변수 정의에서 정규화된 이름을 사용하여 IDE 문제를 해결할 수 있다.
또한 가져온 유형을 사용하여 표현식의 일부를 변환할 수도 있습니다. 다음 예는 connection 속성을 User 유형으로 변환한다.
또한 표현식에서 정적 필드 및 메서드를 참조할 때 가져온 유형을 사용할 수도 있다. 아래 코드는 MyStringUtils 클래스를 가져와서 capitalize 메서드를 참조한다.
…
data 요소 내에서 여러 variable 요소를 사용할 수 있다. 각 variable 요소는 레이아웃 파일 내 결합 표현식에 사용될 레이아웃에서 설정할 수 있는 속성을 설명한다. 아래 예는 user, image 및 note 변수를 선언한다.
변수 유형은 컴파일 타임에 검사된다. 따라서 변수가 Observable을 구현하거나 식별 가능한 컬렉션이라면 유형에 반영된다. 변수가 Observable 인터페이스를 구현하지 않는 기본 클래스 또는 인터페이스라면 변수들이 관찰되지 않는다. 다양한 구성(예: 가로 모드 또는 세로 모드)의 레이아웃 파일이 서로 다를 때 변수가 결합된다. 이러한 레이아웃 파일 간에 충돌하는 변수 정의가 있어서는 안된다. 생성된 결합 클래스에는 설명된 각 변수의 setter 및 getter가 있다. 변수는 setter가 호출될 때까지 기본 관리형 코드 값을 사용한다. 예를 들어 참조 유형은 null을, int는 0을, boolean은 false를 기본값으로 사용한다. 필요에 따라 결합 표현식에 사용하기 위해 context라는 이름의 특수 변수를 생성한다. context의 값은 루트 뷰의 getContext() 메서드에서 온 Context 객체이다. context 변수가 이 이름을 사용하는 명시적 변수 선언으로 재정의된다.
속성에 앱 네임스페이스 및 변수 이름을 사용함으로써 포함하는 레이아웃에서 포함된 레이아웃의 결합으로 변수를 전달할 수 있다. 다음 예는 name.xml 및 contact.xml 레이아웃 파일로부터 포함된 user 변수를 보여준다.
Data binding은 Includes를 병합 요소의 직접 하위 요소로 지원하지 않는다. 예를 들어 아래 레이아웃은 지원되지 않는다.
Binding adapter는 적절한 프레임워크를 호출하여 값을 설정하는 작업을 담당한다. 한 가지 예로 setText() 메서드를 호출하는 것과 같이 속성 값을 설정하는 작업을 들 수 있다. 또 다른 예로는 setOnClickListener() 메서드를 호출하는 것과 같이 이벤트 리스너를 설정하는 작업이 있다. Data Binding 라이브러리를 사용하면 값을 설정하기 위해 호출되는 메서드를 지정하고 고유한 Binding 로직을 제공하며 어댑터를 사용함으로써 반환된 객체의 유형을 지정할 수 있다.
결합된 값이 변경될 때마다 생성된 결합 클래스는 결합 표현식을 사용하여 뷰에서 setter 메서드를 호출해야 한다. Data Binding 라이브러리에서 메서드를 자동으로 결정하거나 메서드를 명시적으로 선언하거나 맞춤 로직을 제공해 메서드를 선택하도록 허용할 수 있다.
이름이 example인 속성의 경우 라이브러리는 호환 가능한 유형을 인수로 허용하는 setExample(arg) 메서드를 자동으로 찾으려고 한다. 속성의 네임스페이스는 고려되지 않으며 메서드 검색 시 속성 이름 및 유형만 사용된다. 예를 들어 android:text="@{user.name}" 표현식이 있다고 한다면 라이브러리는 user.getName()에서 반환한 유형을 허용하는 setText(arg) 메서드를 찾는다. user.getName()의 반환 유형이 String이면 라이브러리는 String 인수를 허용하는 setText() 메서드를 찾는다. 표현식이 int를 대신 반환하면 라이브러리는 int 인수를 허용하는 setText() 메서드를 검색한다. 표현식은 올바른 유형을 반환해야 한다. 필요하다면 반환 값을 변환할 수 있다. 지정된 이름의 속성이 없더라도 데이터 결합은 작동한다. 그때는 데이터 결합을 사용하여 setter에 필요한 속성을 생성할 수 있다. 예를 들어 지원 클래스 DrawerLayout에는 어떤 속성도 없지만 많은 setter가 있다. 아래 레이아웃은 자동으로 setScrimColor(int) 및 setDrawerListener(DrawerListener) 메서드를 각각app:scrimColor 및 app:drawerListener 속성의 setter로 사용한다.
일부 속성에는 이름이 일치하지 않는 setter가 있다. 이러한 상황에서 속성은 BindingMethods 주석을 사용하여 setter와 연결될 수 있다. 주석은 클래스와 함께 사용되며 이름이 바뀐 각 메서드에 하나씩 여러 BindingMethod 주석을 포함할 수 있다. 결합 메서드는 앱의 어떤 클래스에도 추가할 수 있는 주석이다. 아래 예에서 android:tint 속성은 setTint() 메서드가 아닌 setImageTintList(ColorStateList) 메서드와 연결된다.
@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])
일반적으로 Android 프레임워크 클래스에서 setter의 이름을 바꿀 필요가 없다. 이름 규칙을 사용하여 일치하는 메서드를 자동으로 찾는 속성이 이미 구현되어있다.
일부 속성에는 맞춤 결합 로직이 필요하다. 예를 들어 android:paddingLeft 속성에는 연결된 setter가 없는 대신 setPadding(left, top, right, bottom) 메서드가 제공된다. BindingAdapter 주석이 있는 정적 결합 어댑터 메서드를 사용하면 속성의 setter가 호출되는 방식을 맞춤 설정할 수 있다. Android 프레임워크 클래스의 속성에는 BindingAdapter 주석이 이미 생성되어 있다. 아래 코드는 paddingLeft 속성의 결합 어댑터를 보여준다.
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
매개변수 유형은 중요하다. 첫 번째 매개변수는 속성과 연결된 뷰의 유형을 결정한다. 두 번째 매개변수는 지정된 속성의 결합 표현식에서 허용되는 유형을 결정한다. BindingAdapter는 다른 유형의 맞춤설정에 유용하다. 예를 들어 맞춤 로더는 작업자 스레드에서 호출되어 이미지를 로드할 수 있다. 개발자가 정의하는 BindingAdapter는 충돌이 발생하면 Android 프레임워크에서 제공하는 기본 어댑터보다 우선 적용된다. 또한 아래 예에서와 같이 여러 속성을 받는 어댑터도 있을 수 있다.
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
위 BindingAdapter는 레이아웃에서 아래와 같이 사용할 수 있다. 여기서 @drawable/venueError는 앱의 리소스를 나타낸다. 리소스를 @{}로 묶으면 유효한 결합 표현식이 된다.
imageUrl과 error가 모두 ImageView 객체에 사용되는데 imageUrl은 문자열이고 error는 Drawable이라면 어댑터가 호출된다. 어떤 속성이라도 설정될 때 어댑터를 호출하려면 다음 예에서와 같이 어댑터의 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 메서드는 선택적으로 핸들러의 이전 값을 사용할 수 있다. 이전 값과 새 값을 사용하는 메서드는 아래 예에서와 같이 속성의 모든 이전 값을 먼저 선언한 후 새 값을 선언해야 한다.
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
if (oldPadding != newPadding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
}
이벤트 핸들러는 예에서와 같이 하나의 추상 메서드가 있는 인터페이스 또는 추상 클래스에서만 사용할 수 있다.
리스너에 여러 메서드가 있으면 여러 리스너로 분할해야 한다. 예를 들어 View.OnAttachStateChangeListener에는 onViewAttachedtoWindow(View) 및 onViewDetachedFromWindow(View) 2개 메서드가 있다. 라이브러리는 2개의 인터페이스를 제공하여 이러한 메서드의 속성 및 핸들러를 구별한다.
// Translation from provided interfaces in Java:
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
fun onViewDetachedFromWindow(v: View)
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
fun onViewAttachedToWindow(v: View)
}
하나의 리스너를 변경하면 다른 리스너에도 영향을 줄 수 있으므로 어느 한 속성 또는 둘 다에서 작동하는 어댑터가 필요하다. 아래 예에서와 같이 주석에서 requireAll을 false로 설정하여 모든 속성에 결합 표현식을 할당할 필요는 없다는 것을 지정할 수 있다.
@BindingAdapter(
"android:onViewDetachedFromWindow",
"android:onViewAttachedToWindow",
requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
val newListener: View.OnAttachStateChangeListener?
newListener = if (detach == null && attach == null) {
null
} else {
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
attach.onViewAttachedToWindow(v)
}
override fun onViewDetachedFromWindow(v: View) {
detach.onViewDetachedFromWindow(v)
}
}
}
val oldListener: View.OnAttachStateChangeListener? =
ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener)
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener)
}
}
}
Object가 결합 표현식에서 반환되면 라이브러리는 속성 값을 설정하는 데 사용되는 메서드를 선택한다. Object는 선택된 메서드의 매개변수 유형으로 변환된다. 이 동작은 아래 예에서와 같이 ObservableMap클래스를 사용하여 데이터를 저장하는 앱에서 유용하다.
참고: object.key 표기법을 사용하여 맵에서 값을 참조할 수도 있다. 예를 들어 위의 예에서 @{userMap["lastName"]}을 @{userMap.lastName}으로 대체할 수 있다.
어떤 상황에서는 특정 유형 간에 맞춤 변환이 필요하다. 뷰의 android:background 속성에 Drawable이 필요한데 지정된 color 값이 정수인 상황을 예로 들 수 있다. 아래 예는 Drawable이 필요한데 정수가 대신 지정된 속성을 보여준다.
Drawable이 필요한데 정수가 반환될 때마다 int가 ColorDrawble로 변환되어야 한다. 아래와 같이 BindingConversion 주석이 있는 정적 메서드를 사용하여 변환을 실행할 수 있다.
@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)
단, 결합 표현식에 지정하는 값 유형은 일관되어야 하므로 아래 예에서와 같이 동일한 표현식에 서로 다른 유형을 사용할 수 없다.
AndroidX 라이브러리에는 성능이 뛰어나고 테스트와 유지관리가 쉬운 앱을 디자인하는 데 사용할 수 있는 Architecture Components가 포함되어 있다. Data binding 라이브러리는 Architecture Components와 원활하게 연동하여 UI 개발을 더욱 단순화한다. 앱의 레이아웃은 이미 UI 컨트롤러 수명 주기를 관리하고 데이터의 변경을 알리도록 돕는 아키텍처 구성요소의 데이터에 결합할 수 있다. 해당 항목에서는 앱에 Architecture Components를 통합하여 Data binding 라이브러리 사용의 이점을 더 강화하는 방법을 설명한다.
LiveData 객체를 데이터 결합 소스로 사용하여 데이터 변경을 UI에 자동으로 알릴 수 있다. Observable fields와 같이 Observable을 구현하는 객체와 달리 LiveData 객체는 데이터 변경을 구독하는 관찰자의 수명 주기를 알고 있다. 이 수명 주기를 알면 LivaData의 이점을 활용할 수 있다. Android 스튜디오 버전 3.1 이상에서는 데이터 결합 코드에서 Observable fields를 LiveData 객체로 바꿀 수 있다. 결합 클래스와 함께 LiveData 객체를 사용하려면 수명 주기 소유자를 지정하여 LiveData 객체의 범위를 정의해야 한다. 아래 예에서는 결합 클래스를 인스턴스화한 후 활동을 수명 주기 소유자로 지정한다.
class ViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Inflate view and obtain an instance of the binding class.
val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)
}
}
ViewModel 구성요소를 사용하여 데이터를 레이아웃에 결합할 수 있고 LiveData 객체를 사용하여 데이터를 변환하거나 여러 데이터 소스를 병합할 수 있다. 아래 예는 ViewModel에서 데이터를 변환하는 방법을 보여준다.
class ScheduleViewModel : ViewModel() {
val userName: LiveData
init {
val result = Repository.userName
userName = Transformations.map(result) { result -> result.value }
}
}
Data binding 라이브러리는 ViewModel 구성요소와 원활하게 연동한다. 이 구성요소는 레이아웃이 관찰하고 변경사항에 반응하는 데이터를 노출한다. Data binding 라이브러리와 함께 ViewModel 구성요소를 사용하면 UI 로직을 레이아웃에서 구성요소로 쉽게 이동할 수 있으므로 테스트하기가 더 쉽다. Data binding 라이브러리를 사용하면 필요할 때 뷰를 데이터 소스에 결합하고 데이터 소스에서 결합 해제할 수 있다. Data binding 라이브러리를 사용하는 그 밖의 작업은 대부분 적절한 데이터를 노출하고 있는지 확인하는 것이다. Data binding 라이브러리와 함께 ViewModel 구성요소를 사용하려면 ViewModel 클래스에서 상속받는 구성요소를 인스턴스화하고 결합 클래스의 인스턴스를 가져와 결합 클래스의 속성에 ViewModel 구성요소를 할당해야 한다. 아래 예는 라이브러리와 함께 구성요소를 사용하는 방법을 보여준다.
class ViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Obtain the ViewModel component.
UserModel userModel = ViewModelProviders.of(getActivity())
.get(UserModel.class)
// Inflate view and obtain an instance of the binding class.
val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)
// Assign the component to a property in the binding class.
binding.viewmodel = userModel
}
}
아래 예에서와 같이 레이아웃에서 결합 표현식을 사용하여 적절한 뷰에 ViewModel 구성요소의 속성 및 메서드를 할당한다.
Observable을 구현하는 ViewModel 구성요소를 사용하면 LiveData 객체를 사용하는 방식과 유사하게 데이터 변경을 다른 앱 구성요소에 알릴 수 있다. LiveData의 수명 주기 관리 기능이 손실되었더라도 LiveData 객체를 사용하는 것보다 Observable 인터페이스를 구현하는 ViewModel 구성요소를 사용하는 것이 더 좋은 상황도 있다. Observable을 구현하는 ViewModel 구성요소를 사용하면 앱의 결합 어댑터를 더 세밀하게 제어할 수 있다. 예를 들어 이 패턴을 사용하면 데이터가 변경될 때 알림을 더 세밀하게 제어할 수 있으며 맞춤 메서드를 지정하여 양방향 데이터 결합의 속성 값을 설정할 수도 있다. 관찰 가능한 ViewModel 구성요소를 구현하려면 ViewModel 클래스에서 상속받고 Observable 인터페이스를 구현하는 클래스를 생성해야 한다. 관찰자가 addOnPropertyChangedCallback() 및 removeOnPropertyChangedCallback() 메서드를 사용하여 알림을 구독하거나 구독 취소할 때 맞춤 로직을 제공할 수 있다. 또한 notifyPropertyChanged() 메서드에서 속성이 변경될 때 실행되는 맞춤 로직을 제공할 수도 있다. 아래 코드 예는 관찰 가능한 ViewModel을 구현하는 방법을 보여준다.
/**
* A ViewModel that is also an Observable,
* to be used with the Data Binding Library.
*/
open class ObservableViewModel : ViewModel(), Observable {
private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
override fun addOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
override fun removeOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
/**
* Notifies observers that all properties of this instance have changed.
*/
fun notifyChange() {
callbacks.notifyCallbacks(this, 0, null)
}
/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the @Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* @param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
양방향 데이터 결합은 이 프로세스의 바로가기를 제공한다. '=' 기호가 포함된 @={} 표기법은 속성과 관련된 데이터 변경사항을 받는 동시에 사용자 업데이트를 수신한다.
백업 데이터의 변경에 대응하기 위해 다음 코드 스니펫에서와 같이 레이아웃 변수를 Observable 일반적으로
BaseObservable의 구현으로 만들고 @Bindable 주석을 사용할 수 있습니다.
class LoginViewModel : BaseObservable {
// val data = ...
@Bindable
fun getRememberMe(): Boolean {
return data.rememberMe
}
fun setRememberMe(value: Boolean) {
// Avoids infinite loops.
if (data.rememberMe != value) {
data.rememberMe = value
// React to the change.
saveData()
// Notify observers of a new value.
notifyPropertyChanged(BR.remember_me)
}
}
}
결합 가능한 속성의 getter 메서드는 getRememberMe()라고 하므로 속성의 상응하는 setter 메서드는 자동으로 setRememberMe()라는 이름을 사용한다.
플랫폼은 앱의 일부로 사용할 수 있는 가장 일반적인 양방향 속성 및 변경 리스너의 양방향 데이터 결합 구현을 제공한다. 맞춤 속성으로 양방향 데이터 결합을 사용하려면 @InverseBindingAdapter 및 @InverseBindingMethod를 활용해야 한다. 예를 들어 MyView라는 맞춤 뷰에서 "time" 속성에 양방향 데이터 결합을 사용하려면 아래 단계를 완료해야 합니다.
[Step 1] 초기 값을 설정하고 값이 변경될 때 업데이트하는 메서드에 @BindingAdapter를 사용하여 주석을 추가한다.
@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue
}
}
[Step 2] 뷰에서 값을 읽는 메서드에 @InverseBindingAdapter를 사용하여 주석을 추가한다.
@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
return view.getTime()
}
이 시점에서 데이터 결합은 데이터가 변경될 때 해야 할 작업(@BindingAdapter를 사용하여 주석을 추가한 메서드 호출)과 뷰 속성이 변경될 때 호출할 대상(InverseBindingListener 호출)을 알고 있지만 속성이 언제 어떻게 변경되는지는 알 수 없으므로 속성의 변경 시기 또는 방식을 알기 위해서는 뷰에 리스너를 설정해야 한다. 리스너는 맞춤 뷰와 연결된 맞춤 리스너이거나 포커스 상실 또는 텍스트 변경과 같은 제네릭 이벤트일 수 있다. 다음과 같이 속성 변경의 리스너를 설정하는 메서드에 @BindingAdapter 주석을 추가한다.
@BindingAdapter("app:timeAttrChanged")
@JvmStatic fun setListeners(
view: MyView,
attrChange: InverseBindingListener
) {
// Set a listener for click, focus, touch, etc.
}
리스너에는 InverseBindingListener가 매개변수로 포함되며 InverseBindingListener를 사용하면 데이터 결합 시스템에 속성이 변경되었음을 알릴 수 있다. 그러면 시스템은 @InverseBindingAdapter 등을 사용하여 주석이 추가된 메서드 호출을 시작할 수 있다.
View 개체에 결합된 변수를 표시하기 전에 먼저 형식 지정, 변환 또는 변경을 해야 하면 Converter 개체를 사용하면 된다. 예를 들어 날짜를 보여주는 EditText 개체를 사용한다.
viewmodel.birthDate 속성에는 Long 유형의 값이 포함되어 있으므로 변환기를 사용하여 형식을 지정해야 한다. 또한 양방향 표현식을 사용 중이므로 사용자가 제공한 문자열을 백업 데이터 유형(이 사례에서는 Long)으로 다시 변환하는 방법을 라이브러리에 알려주는 역변환기도 있어야 한다. 이 프로세스는 변환기 중 하나에 @InverseMethod를 추가하고 이 주석이 역변환기를 참조하도록 함으로써 완료할 수 있다. 아래 코드 스니펫은 이 구성의 예를 보여준다.
object Converter {
@InverseMethod("stringToDate")
fun dateToString(
view: EditText, oldValue: Long,
value: Long
): String {
// Converts long to String.
}
fun stringToDate(
view: EditText, oldValue: String,
value: String
): Long {
// Converts String to long.
}
}
양방향 데이터 결합을 사용할 때 무한 루프가 발생하지 않도록 주의해야 한다. 사용자가 속성을 변경하면 @InverseBindingAdapter를 사용하여 주석이 추가된 메서드가 호출되고 값이 backing 속성에 할당되면 결과적으로 @BindingAdapter를 사용하여 주석이 추가된 메서드가 호출되어 @InverseBindingAdapter를 사용하여 추가된 메서드의 또 다른 호출이 트리거 된다. 이러한 이유로 @BindingAdapter를 사용하여 주석이 추가된 메서드의 새 값과 이전 값을 비교함으로써 발생 가능한 무한 루프를 끊는 것이 중요하다.
아해 표의 속성을 사용할 때 플랫폼은 기본적으로 양방향 데이터 결합을 지원한다. 플랫폼에서 이 지원을 제공하는 방식에 관한 자세한 내용은 해당하는 결합 어댑터의 구현을 참조! 클래스 속성 결합 어댑터 AdapterView android:selectedItemPosition android:selection AdapterViewBindingAdapter CalendarView android:date CalendarViewBindingAdapter CompoundButton android:checked CompoundButtonBindingAdapter DatePicker android:year android:month android:day DatePickerBindingAdapter NumberPicker android:value NumberPickerBindingAdapter RadioButton android:checkedButton RadioGroupBindingAdapter RatingBar android:rating RatingBarBindingAdapter SeekBar android:progress SeekBarBindingAdapter TabHost android:currentTab TabHostBindingAdapter TextView android:text TextViewBindingAdapter TimePicker android:hour android:minute TimePickerBindingAdapter
https://medium.com/@PaperEd/android-how-to-databinding-169c78e7dc28 https://developer.android.com/topic/libraries/data-binding/expressions