봄날은 갔다. 이제 그 정신으로 공부하자

DataBinding을 사용해 앱을 더 심플하게 개발하기 본문

Android jetpack

DataBinding을 사용해 앱을 더 심플하게 개발하기

길재의 그 정신으로 공부하자 2020. 11. 23. 18:09

초창기 android는 비지니스 로직과 View로직의 분리를 구조적으로 지원하지 않고 개발 구조를 개발자들에 위임하였습니다.

숙련도 있는 일부 개발자들은 높은 자유도를 활용해 높은 안정성에 우수한 성능을 자랑하는 앱을 개발하였지만 대다수의 개발자들은 비지니스 로직과 View 로직을 분리하지 않고 기능 개발에 급급했습니다.

그러다 보니 마켓에 배포된 앱들에서 ANR과 비정상 종료를 보는 것은 어려운 일이 아니었습니다.

이러한 문제를 개선하기 위해 google은 MainThread에서 Network 및 DB 엑세스 제한, 백그라운드 프로세스 실행 제한 등과 같은 많은 제한을 두어 이러한 문제를 해결하였지만 반대로 개발 난이도의 상승을 불러 일으키게 되었습니다.

 

이에 google은 개발자 지원 정책으로 다양한 라이브러리를 지원하였는데 그중 한개가 해당 글에서 설명하는 DataBinding 입니다.

 

DataBinding은 xml에 데이터를 바인딩하여 불필요한 코드를 줄이는 방법으로, 보통 MVP or MVVM 패턴을 구현 할 때 사용하며 Android API Level7 이후(2009년 Eclair)부터 지원된 라이브러리 입니다.

 

DataBinding 라이브러리는 프로그래매틱 방식이 아니라 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합할 수 있는 지원 라이브러리로 기존 사용하던 방식과 달리 간단하게 UI 구성 요소 접근 및 제어가 가능합니다.

예를 들어 아래 코드는 Activity에서 findViewById()를 호출하여 TextView 위젯을 찾아 viewModel 변수의 userName 속성에 결합하는 예입니다.

findViewById<TextView>(R.id.sample_text).apply {

        text = viewModel.userName
}

하지만 아래와 같이 DataBinding 라이브러리를 사용하면 위와 같이 Activity에서 레이아웃의 UI 구성요소를 접근할 필요 없이 레이아웃에서 직접 간단하게 처리가 가능합니다.

<TextView

        android:text="@{viewmodel.userName}" />

위와 같이 DataBinding 라이브러리를 사용해 레이아웃 파일에서 구성요소를 결합하면 활동에서 많은 UI 프레임워크 호출을 삭제할 수 있어 파일이 더욱 단순화되고 유지관리 또한 쉬워지며 앱 성능이 향상되고 메모리 누수 및 null 포인터 예외를 방지할 수 있습니다.

 

DataBinding 사용하기

[STEP-1] DataBinding 설정

DataBindng 라이브러리를 사용하기 위해서는 build.gradle 파일 상단에 아래와 같은 코드를 추가해야 합니다.

apply plugin: "kotlin-kapt"

android {
    ....
    dataBinding {
        enabled = true
    }
}

 

[STEP-2] DataBinding을 위한 ViewModel 만들기

xml에 연동할 ViewModel.kt 파일을 아래와 같이 생성합니다.

class ViewModel {

    val text = ObservableField<String>("")
    fun showText(view: View) {
        Toast.makeText(view.context, "${text.get()}", Toast.LENGTH_SHORT).show()
    }
}

ViewModel class에 작성한 showText() 함수는 xml에서 호출 가능합니다.

단, 접근 제한자가 private인 경우, 호출 불가능합니다.

 

[STEP-3] layout 파일 수정하기

DataBinding을 사용하기 위해서는 아래와 같이 루트 태그를 <layout>으로 변경하고

<data> 태그 블록 안에 <variable> 태그를 사용해 layout에 연동할 ViewModel을 지정해야 합니다.

 

ViewModel의 데이터에 접근 할 때에는, @{} 안에 참조 할 데이터를 작성하면 됩니다.

DataBinding을 사용하면 레이아웃에서 간단한 식을 사용할 수 있습니다. (아래 코드의 android:visiblity 참고)

 

또한 DataBinding에서는 android:onClick등의 이벤트가 발생했을 때 수행할 동작을 바인딩 할 수 있습니다.

함수를 바인딩 할 때에는, @{() -> vm.doSomething()} 같이 람다식을 사용해서 호출합니다.

이 때 함수는 이벤트의 파라미터를 받거나 context 를 파라미터로 받아서 사용 할 수 있습니다.

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="android.view.View" />
        <variable
            name="vm"
            type="com.github.papered.a2waytest.ViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="text"
            android:text="@={vm.text}" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{vm.text}" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{(view) -> vm.showText(view) }"
            android:text="Touch!"
            android:visibility="@{vm.text.length > 5 ? View.VISIBLE    : View.GONE}"/>
    </LinearLayout>
</layout>

바인딩을 위해서는 Activity 파일에 아래의 코드를 작성합니다.

val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

binding.vm = ViewModel()

setContentView는 DataBinding을 하며 써주기 때문에, 작성 할 필요가 없으니 지워줍니다.

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: 이벤트가 발생할 때 계산되는 람다 표현식이며 데이터 결합은 항상 리스너를 생성하여 뷰에서 설정합니다.

          이벤트가 전달되면 리스너는 람다 표현식을 계산합니다.

 

Method reference

이벤트는 android:onClick이 활동의 메서드에 할당되는 방식과 유사하게 핸들러 메서드에 직접 결합될 수 있습니다.

View onClick 속성과 비교했을 때 주요 이점은 표현식이 컴파일 타임에 처리되므로 메서드가 없거나 서명이 올바르지 

않으면 컴파일 타임 오류가 발생한다는 점입니다.

Method reference와  Listener binding의 주요 차이점은 실제 리스너 구현이 이벤트가 트리거될 때가 아니라 

데이터가 결합될 때 생성된다는 점입니다. 이벤트가 발생할 때 표현식을 계산하려면 Listener binding을 사용해야 합니다.

핸들러에 이벤트를 할당하려면 호출할 메서드 이름이 될 값을 사용하여 일반 결합 표현식을 사용해야 합니다.

class MyHandlers {

        fun onClickFriend(view: View) { ... }
}

// 결합 표현식은 다음과 같이 뷰의 클릭 리스너를 onClickFriend() 메서드에 할당할 수 있습니다.
<?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>


Listener binding

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() 메서드에 결합할 수 있다.
<?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>

표현식에 콜백을 사용하면 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가 아닌 유형의 값을 반환하면 표현식도 같은 유형의 값을 반환해야 합니다.

예를 들어 'Long Click' 이벤트를 수신 대기하려면 표현식에서  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}"

 

imports, variables, includes

Data Binding 라이브러리는 Imports, variables, and includes과 같은 기능을 제공gkqslek.

imports를 사용하면 레이아웃 파일 내에서 클래스를 쉽게 참조할 수 있고 

variables를 사용하면 결합 표현식에 사용할 수 있는 속성을 설명할 수 있으며 

includes을 사용하면 앱 전체에서 복잡한 레이아웃을 재사용할 수 있습니다.

 

import

imports를 사용하면 관리형 코드에서와 같이 레이아웃 파일 내에서 클래스를 쉽게 참조할 수 있습니다.

0개 이상의 import 요소를 data 요소 내에서 사용할 수 있습니다.

아래는 View 클래스를 레이아웃 파일로 가져오는 예제 입니다.

<data>

    <import type="android.view.View"/>
</data>

// View 클래스를 가져오면 결합 표현식에서 참조할 수 있다.
<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

 

Type aliases

클래스 이름 충돌이 발생하면 클래스 중 하나의 이름을 별칭으로 바꿀 수 있습니다.

아래는 com.example.real.estate 패키지의 View 클래스 이름을 Vista로 변경하는 에제 입니다.

<import

    type="android.view.View"/>

<import
    type="com.example.real.estate.View"
    alias="Vista"/>

이제 Vista를 사용하여 com.example.real.estate.View를 참조할 수 있습니다.

그리고 레이아웃 파일 내에서 android.view.View를 참조하는 데 View를 사용할 수 있습니다.

 

import other classes

가져온 유형은 변수 및 표현식에서 유형 참조로 사용할 수 있습니다. 

아래 예는 변수의 유형으로 사용되는 User 및 List를 보여줍니다.

<data>

    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List<User>"/>
</data>

주의: Android 스튜디오에서는 아직 가져오기를 처리하지 못하므로 가져온 변수의 자동 완성이 IDE에서 작동하지 않을 수 있으나 앱은 문제 없이 컴파일됩니다. 그리고 변수 정의에서 정규화된 이름을 사용하여 IDE 문제를 해결할 수 있습니다.

또한 가져온 유형을 사용하여 표현식의 일부를 변환할 수도 있습니다.

아래는 connection 속성을 User 유형으로 변환 예제 입니다.

<TextView
   android:text="@{((User)(user.connection)).lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

또한 표현식에서 정적 필드 및 메서드를 참조할 때 가져온 유형을 사용할 수도 있습니다.

아래는 MyStringUtils 클래스를 가져와서 capitalize 메서드를 참조 예제입니다.

<data>
   <import type="com.example.MyStringUtils"/>
   <variable name="user" type="com.example.User"/>
</data>
…
<TextView
   android:text="@{MyStringUtils.capitalize(user.lastName)}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

 

variables

data 요소 내에서 여러 variable 요소를 사용할 수 있습니다.

각 variable 요소는 레이아웃 파일 내 결합 표현식에 사용될 레이아웃에서 설정할 수 있는 속성을 설명합니다.

아래는 user, image 및 note 변수를 선언 예제 입니다.

<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>

변수 유형은 컴파일 타임에 검사되므로 변수가 Observable을 구현하거나 식별 가능한 컬렉션이라면 유형에 반영됩니다.

변수가 Observable 인터페이스를 구현하지 않는 기본 클래스 또는 인터페이스라면 변수들이 관찰되지 않습니다.

다양한 구성(예: 가로 모드 또는 세로 모드)의 레이아웃 파일이 서로 다를 때 변수가 결합되므로

이러한 레이아웃 파일 간에 충돌하는 변수 정의가 있어서는 안됩니다.

생성된 결합 클래스에는 설명된 각 변수의 setter 및 getter가 있습니다.

변수는 setter가 호출될 때까지 기본 관리형 코드 값을 사용합니다.

예를 들어 참조 유형은 null을, int는 0을, boolean은 false를 기본값으로 사용합니다.

필요에 따라 결합 표현식에 사용하기 위해 context라는 이름의 특수 변수를 생성합니다.

context의 값은 루트 뷰의 getContext() 메서드에서 온 Context 객체이며

context 변수가 이 이름을 사용하는 명시적 변수 선언으로 재정의됩니다.

 

includes

속성에 앱 네임스페이스 및 변수 이름을 사용함으로써 포함하는 레이아웃에서 포함된 레이아웃의 결합으로 변수를 전달할 수 있습니다.

아래는 name.xml 및 contact.xml 레이아웃 파일로부터 포함된 user 변수를 보여주는 예제 입니다.

<?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은 Includes를 병합 요소의 직접 하위 요소로 지원하지 않습니다.

예를 들어 아래 레이아웃은 지원되지 않습니다.

<?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>
    <merge><!-- Doesn't work -->
        <include
            layout="@layout/name"
            bind:user="@{user}"/>
        <include
            layout="@layout/contact"
            bind:user="@{user}"/>
    </merge>
</layout>

 

Binding Adapters

Binding adapter는 적절한 프레임워크를 호출하여 값을 설정하는 작업을 담당합니다.

한 가지 예로 setText() 메서드를 호출하는 것과 같이 속성 값을 설정하는 작업을 들 수 있습니다.

또 다른 예로는 setOnClickListener() 메서드를 호출하는 것과 같이 이벤트 리스너를 설정하는 작업이 있습니다.

Data Binding 라이브러리를 사용하면 값을 설정하기 위해 호출되는 메서드를 지정하고 고유한 Binding 로직을 

제공하며 어댑터를 사용함으로써 반환된 객체의 유형을 지정할 수 있습니다.

 

Setting attribute values

결합된 값이 변경될 때마다 생성된 결합 클래스는 결합 표현식을 사용하여 뷰에서 setter 메서드를 호출해야 합니다.

Data Binding 라이브러리에서 메서드를 자동으로 결정하거나 메서드를 명시적으로 선언하거나 맞춤 로직을 제공해 

메서드를 선택하도록 허용할 수 있습니다.

 

Automatic method selection

이름이 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로 사용합니다.

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}">

Specify a custom method name

일부 속성에는 이름이 일치하지 않는 setter가 있는데 이러한 상황에서 속성은 BindingMethods annotation을 사용하여 setter와 연결될 수 있습니다.

annotation은 클래스와 함께 사용되며 이름이 바뀐 각 메서드에 하나씩 여러 BindingMethod주석을 포함할 수 있습니다.

결합 메서드는 앱의 어떤 클래스에도 추가할 수 있는 annotation입니다.

아래 예에서 android:tint 속성은 setTint() 메서드가 아닌 setImageTintList(ColorStateList) 메서드와 연결됩니다.

@BindingMethods(value = [
        BindingMethod(
            type = android.widget.ImageView::class,
            attribute = "android:tint",
            method = "setImageTintList")])

일반적으로 Android 프레임워크 클래스에서 setter의 이름을 바꿀 필요가 없습니다.

이유는 이름 규칙을 사용하여 일치하는 메서드를 자동으로 찾는 속성이 이미 구현되어 있기 때문입니다.

 

Provide custom logic

일부 속성에는 맞춤 결합 로직이 필요합니다.

예를 들어 android:paddingLeft 속성에는 연결된 setter가 없는 대신 setPadding(left, top, right, bottom) 메서드가 제공됩니다. BindingAdapter annotation이 있는 정적 결합 어댑터 메서드를 사용하면 속성의 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는 앱의 리소스 입니다. 리소스를 @{}로 묶으면 유효한 결합 표현식이 됩니다.

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@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)
        }
    }
}

 

Automatic Object conversions

Object가 결합 표현식에서 반환되면 라이브러리는 속성 값을 설정하는 데 사용되는 메서드를 선택하는데

Object는 선택된 메서드의 매개변수 유형으로 변환됩니다.

이 동작은 아래 예에서와 같이 ObservableMap클래스를 사용하여 데이터를 저장하는 앱에서 유용합니다.

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

참고: object.key 표기법을 사용하여 맵에서 값을 참조할 수도 있습니다.

예를 들어 위의 예에서 @{userMap["lastName"]}을 @{userMap.lastName}으로 대체할 수 있습니다.

 

Custom conversions

어떤 상황에서는 특정 유형 간에 맞춤 변환이 필요하다. 뷰의 android:background 속성에 Drawable이 필요한데 

지정된 color 값이 정수인 상황을 예로 들 수 있습니다.

아래 예는 Drawable이 필요한데 정수가 대신 지정된 속성을 보여줍니다.

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Drawable이 필요한데 정수가 반환될 때마다 int가 ColorDrawble로 변환되어야 합니다.

아래와 같이 BindingConversion 주석이 있는 정적 메서드를 사용하여 변환을 실행할 수 있습니다.

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

단, 결합 표현식에 지정하는 값 유형은 일관되어야 하므로 아래 예에서와 같이 동일한 표현식에 서로 다른 유형을 사용할 수 없습니다.

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

 

Bind layout views to Architecture Components

AndroidX 라이브러리에는 성능이 뛰어나고 테스트와 유지관리가 쉬운 앱을 디자인하는 데 사용할 수 있는 Architecture Components가 포함되어 있습니다.

Data binding 라이브러리는 Architecture Components와 원활하게 연동하여 UI 개발을 더욱 단순화합니다.

앱의 레이아웃은 이미 UI 컨트롤러 수명 주기를 관리하고 데이터의 변경을 알리도록 돕는 아키텍처 구성요소의 데이터에 결합할 수 있습니다.

해당 항목에서는 앱에 Architecture Components를 통합하여 Data binding 라이브러리 사용의 이점을 더 강화하는 방법을 설명합니다.

 

Use LiveData to notify the UI about data change

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 }
    }
}

 

Use ViewModel to manage UI-releated data

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 구성요소의 속성 및 메서드를 할당합니다.

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@{viewmodel.rememberMe}"
    android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />

 

Use an Observable ViewModel for more control over binding adapters

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)
    }
}

 

Two-way data binding

양방향 데이터 결합은 이 프로세스의 바로가기를 제공합니다.

'=' 기호가 포함된 @={} 표기법은 속성과 관련된 데이터 변경사항을 받는 동시에 사용자 업데이트를 수신할 수 있습니다.

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@={viewmodel.rememberMe}"/>

백업 데이터의 변경에 대응하기 위해 다음 코드 스니펫에서와 같이 레이아웃 변수를 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()라는 이름을 사용합니다.

 

Two-way data binding using custom attributes

플랫폼은 앱의 일부로 사용할 수 있는 가장 일반적인 양방향 속성 및 변경 리스너의 양방향 데이터 결합 구현을 제공합니다. 

맞춤 속성으로 양방향 데이터 결합을 사용하려면 @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 등을 사용하여 주석이 추가된 메서드 호출을 시작할 수 있습니다.

 

Converters

View 개체에 결합된 변수를 표시하기 전에 먼저 형식 지정, 변환 또는 변경을 해야 하면 Converter 개체를 사용하면 됩니다.

예를 들어 날짜를 보여주는 EditText 개체를 사용합니다.

<EditText
    android:id="@+id/birth_date"
    android:text="@={Converter.dateToString(viewmodel.birthDate)}"/>

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.
    }
}


Infinite loops using two-way data binding

양방향 데이터 결합을 사용할 때 무한 루프가 발생하지 않도록 주의해야 합니다.

사용자가 속성을 변경하면 @InverseBindingAdapter를 사용하여 주석이 추가된 메서드가 호출되고 값이 backing 속성에 

할당되면 결과적으로 @BindingAdapter를 사용하여 주석이 추가된 메서드가 호출되어 @InverseBindingAdapter를 

사용하여 추가된 메서드의 또 다른 호출이 트리거 됩니다.

이러한 이유로 @BindingAdapter를 사용하여 주석이 추가된 메서드의 새 값과 이전 값을 비교함으로써 발생 가능한 무한 루프를  끊는 것이 중요합니다.

 

Two-way attributes

아래 표의 속성을 사용할 때 플랫폼은 기본적으로 양방향 데이터 결합을 지원합니다.

Comments