Join us on the livestream at Android Dev Summit on 7-8 November 2018, starting at 10AM PDT!

데이터 바인딩 라이브러리

이 문서에서는 데이터 바인딩 라이브러리를 사용하여 선언적 레이아웃을 작성하고 애플리케이션 로직과 레이아웃을 바인딩하는 데 필요한 글루 코드를 최소화하는 방법을 설명합니다.

데이터 바인딩 라이브러리는 유연성과 폭넓은 호환성을 모두 제공하는 지원 라이브러리로, Android 2.1(API 레벨 7 이상)까지 Android 플랫폼의 모든 이전 버전에서 사용할 수 있습니다.

데이터 바인딩을 사용하려면 Android Plugin for Gradle 1.5.0-alpha1 이상이 필요합니다.

빌드 환경

데이터 바인딩을 시작하려면 Android SDK Manager의 Support 저장소에서 라이브러리를 다운로드하세요.

데이터 바인딩을 사용하도록 앱을 구성하려면 앱 모듈의 build.gradle 파일에 dataBinding 요소를 추가하세요.

다음 코드 조각을 사용하여 데이터 바인딩을 구성합니다.

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

데이터 바인딩을 사용하는 라이브러리에 따라 다른 앱 모듈이 있는 경우, 앱 모듈은 build.gradle 파일에서도 데이터 바인딩을 구성해야 합니다.

사용 중인 Android Studio 버전이 호환 가능한지도 확인하세요. Android Studio 1.3 이상에서는 Android Studio의 데이터 바인딩 지원에 설명되어 있는 데이터 바인딩 지원이 제공됩니다.

데이터 바인딩 레이아웃 파일

첫 번째 데이터 바인딩 식 작성

데이터 바인딩 레이아웃 파일은 약간 달라서 layout의 루트 태그로 시작하고 그 뒤에 data 요소와 view 루트 요소가 나옵니다. 이 view 요소는 루트가 바인딩 레이아웃 파일이 아닌 파일에 있는 요소입니다. 아래 샘플 파일을 참조하세요.

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

data 내에 있는 user 변수는 이 레이아웃 내에서 사용할 수 있는 속성에 대한 설명입니다.

<variable name="user" type="com.example.User"/>

레이아웃 내에 있는 식은 "@{}" 구문을 사용하여 특성 속성에 기록됩니다. 여기서 TextView의 텍스트는 사용자의 firstName 속성으로 설정됩니다.

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"/>

데이터 객체

User에 대한 POJO(plain-old Java object)가 있는 것으로 가정해 봅시다.

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}

이 형식의 객체에는 결코 변경되지 않는 데이터가 있습니다. 애플리케이션에는 흔히 한 번만 읽히고 그 후로는 변경되지 않는 데이터가 있습니다. 다음과 같이 JavaBeans 객체를 사용할 수도 있습니다.

public class User {
   private final String firstName;
   private final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
   public String getFirstName() {
       return this.firstName;
   }
   public String getLastName() {
       return this.lastName;
   }
}

데이터 바인딩의 관점에서 볼 때, 이 두 클래스는 동일합니다. TextView의 android:text 특성에 사용되는 식 @{user.firstName}은 전자의 클래스에 있는 firstName 필드와 후자의 클래스에 있는 getFirstName() 메서드에 액세스합니다. 또는 firstName() 메서드가 존재할 경우에는 이 메서드로 해석하기도 합니다.

데이터 바인딩

기본적으로, Binding 클래스는 레이아웃 파일의 이름을 기준으로 생성되어 파일 이름을 파스칼 표기법(Pascal Case: 합성어의 첫 글자를 대문자로 표기)으로 변환하고 그 뒤에 "Binding"을 접미사로 붙입니다. 위의 레이아웃 파일은 main_activity.xml이고, 따라서 생성된 클래스는 MainActivityBinding이었습니다. 이 클래스는 레이아웃 속성(예: user 변수)에서 레이아웃의 View까지 모든 바인딩을 유지하고 바인딩 식에 대해 값을 할당하는 방법을 알고 있습니다. 바인딩을 가장 쉽게 생성하는 방법은 다음과 같이 확장하는 동안 바인딩을 생성하는 것입니다.

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
}

모두 마쳤습니다! 이제 애플리케이션을 실행하면 UI에 Test User가 보일 겁니다. 또는 다음을 통해 뷰를 얻을 수 있습니다.

MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());

ListView 어댑터나 RecyclerView 어댑터 내에서 데이터 바인딩 항목을 사용 중인 경우 다음을 선호하는 개발자도 있습니다.

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

이벤트 처리

데이터 바인딩을 사용하여 뷰에서 발송되는 이벤트를 처리하는 식을 작성할 수 있습니다(예: onClick). 이벤트 특성 이름은 몇 가지 예외를 제외하면 리스너 메서드의 이름에 따라 결정됩니다. 예를 들어, View.OnLongClickListener에는 메서드 onLongClick()이 있으므로, 이 이벤트에 대한 특성은 android:onLongClick입니다. 이벤트 처리 방법은 다음 두 가지가 있습니다.

  • 메서드 참조: 식에서 리스너 메서드의 서명을 준수하는 메서드를 참조할 수 있습니다. 식이 메서드 참조로 평가되면 데이터 바인딩은 메서드 참조와 소유자 객체를 리스너에 래핑하고 대상 뷰에서 리스너를 설정합니다. 식이 null로 평가되면 데이터 바인딩은 리스너를 생성하지 않고, 그 대신 null 리스너를 설정합니다.
  • 리스너 바인딩: 리스너 바인딩은 이벤트 발생 시 계산되는 람다 식입니다. 데이터 바인딩은 뷰에서 설정하는 리스너를 항상 생성합니다. 이 리스너는 이벤트가 발송될 때 람다 식을 계산합니다.

메서드 참조

Activity에 있는 메서드에 android:onClick을 할당하는 것과 비슷한 방법으로 이벤트를 핸들러 메서드에 직접 바인딩할 수 있습니다. View#onClick 특성에 비해 한 가지 주요 장점은 컴파일 시에 식이 처리되므로, 메서드가 존재하지 않거나 메서드의 서명이 올바르지 않을 경우 컴파일 시 오류가 발생한다는 점입니다.

메서드 참조와 리스너 바인딩의 주요 차이점은 이벤트가 트리거될 때가 아니라, 데이터가 바인딩될 때 실제 리스너 구현이 생성된다는 점입니다. 이벤트 발생 시 식을 계산하려면 리스너 바인딩을 사용해야 합니다.

이벤트를 핸들러에 할당하려면 호출할 메서드 이름이 되는 값과 함께 일반적인 바인딩 식을 사용합니다. 예를 들어, 다음과 같이 데이터 객체에 두 개의 메서드가 있는 경우

public class MyHandlers {
    public void onClickFriend(View view) { ... }
}

바인딩 식이 View에 대해 click 리스너를 할당할 수 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.Handlers"/>
       <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 객체에 있는 메서드의 서명과 정확히 일치해야 합니다.

리스너 바인딩

리스너 바인딩은 이벤트 발생 시 실행되는 바인딩 식입니다. 메서드 참조와 비슷하지만, 리스너 바인딩을 사용하면 임의의 데이터 바인딩 식을 실행할 수 있습니다. 이 기능은 Android Gradle Plugin for Gradle 버전 2.0 이상에서 사용할 수 있습니다.

메서드 참조에서는 메서드의 매개변수가 이벤트 리스너의 매개변수와 일치해야 합니다. 리스너 바인딩에서는 반환 값만 리스너의 예상 반환 값과 일치해야 합니다(void를 예상하고 있지 않은 경우). 예를 들어, 다음 메서드가 있는 presenter 클래스가 있을 수 있습니다.

public class Presenter {
    public void onSaveClick(Task task){}
}
그러면 다음과 같이 click 이벤트를 클래스에 바인딩할 수 있습니다.
  <?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>

리스너는 식의 루트 요소로만 허용되는 람다 식으로 표현됩니다. 식에서 콜백이 사용될 때 데이터 바인딩이 필요한 리스너를 자동으로 생성하고 이벤트에 등록합니다. 뷰가 이벤트를 발생시키면 데이터 바인딩이 주어진 식을 계산합니다. 정규 바인딩 식에서처럼, 이 리스너 식이 평가되는 동안 여전히 데이터 바인딩의 null 및 스레드의 안전이 보장됩니다.

위의 예시에서는 onClick(android.view.View)로 전달되는 view 매개변수를 정의하지 않았습니다. 리스너 바인딩에서는 리스너 매개변수로 두 가지 중에서 선택할 수 있는데, 메서드에 대한 모든 매개변수를 무시하거나 모든 매개변수의 이름을 지정하는 것입니다. 매개변수의 이름을 지정하기로 선택하면 식에 매개변수를 사용할 수 있습니다. 예를 들어, 위의 예시를 다음과 같이 작성할 수 있습니다.

  android:onClick="@{(view) -> presenter.onSaveClick(task)}"
또는 식에 매개변수를 사용할 경우에는 다음과 같이 작동할 수 있습니다.
public class Presenter {
    public void onSaveClick(View view, Task task){}
}
  android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
두 개 이상의 매개변수를 포함한 람다 식을 사용할 수 있습니다.
public class Presenter {
    public void onCompletedChanged(Task task, boolean completed){}
}
  <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

수신 중인 이벤트가 void 형식이 아닌 값을 반환하는 경우, 식도 그와 동일한 형식의 값을 반환해야 합니다. 예를 들어, long click 이벤트를 수신하려는 경우에는 식에서 boolean을 반환해야 합니다.

public class Presenter {
    public boolean onLongClick(View view, Task task){}
}
  android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"

null 객체로 인해 식의 계산이 불가능할 경우, 데이터 바인딩은 그 형식에 대한 기본 Java 값을 반환합니다. 예를 들어, 참조 형식에는 null, int에는 0, boolean에는 false 등을 반환합니다.

조건자(예: ternary)가 있는 식을 사용해야 할 경우 void를 기호로 사용할 수 있습니다.

  android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
복잡한 리스너 방지
리스너 식은 매우 강력하여 개발자가 작성하는 코드가 매우 쉽게 읽히도록 만들 수 있습니다. 반면에, 복잡한 식을 포함한 리스너는 레이아웃을 읽기 어렵고 적절히 유지 관리할 수 없게 만듭니다. 따라서 리스너 식은 UI에서 콜백 메서드로 사용 가능한 데이터를 전달하는 것만큼이나 단순해야 합니다. 비즈니스 로직은 리스너 식에서 호출한 콜백 메서드 내에 구현해야 합니다.

몇 가지 특화된 click 이벤트 핸들러가 있는데, 이런 핸들러는 충돌 방지를 위해 android:onClick 이외의 특성이 필요합니다. 이러한 충돌을 피하기 위해 다음과 같은 특성이 생성되었습니다.

클래스 리스너 Setter 특성
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

레이아웃 세부정보

가져오기

data 요소 내에서는 import 요소가 전혀 사용되지 않거나 한 개 이상 사용될 수 있습니다. Java에서와 마찬가지로, 이러한 요소를 사용하여 레이아웃 파일 내에 있는 클래스를 쉽게 참조할 수 있습니다.

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

클래스 이름 간에 충돌이 발생할 때 해당 클래스 중 하나의 이름을 다음과 같이 "별칭"으로 바꿀 수 있습니다.

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

그러면 레이아웃 파일 내에서 Vista를 사용하여 com.example.real.estate.View를 참조하고 View를 사용하여 android.view.View를 참조할 수 있습니다. 다음과 같이 가져온 형식을 변수와 식의 형식 참조로 사용할 수 있습니다.

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

참고: Android Studio는 아직 가져오기를 처리하지 않으므로, 가져온 변수에 대한 자동 완성 기능이 개발자의 IDE에서 작동하지 않을 수 있습니다. 그래도 애플리케이션이 올바로 컴파일할 것이므로, 변수 정의에서 정규화된 이름을 사용하여 IDE 문제를 해결할 수 있습니다.

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

식에서 정적 필드와 메서드를 참조할 때 가져온 형식을 사용할 수도 있습니다.

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

Java에서와 마찬가지로, java.lang.*을 자동으로 가져오게 됩니다.

변수

data 요소 내에서 사용할 수 있는 variable 요소의 개수에는 제한이 없습니다. 각각의 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>

변수 형식은 컴파일 시에 검사되므로, 변수가 android.databinding.Observable을 구현하거나 변수가 Observable 컬렉션인 경우 그 점이 형식에 반영되어야 합니다. 변수가 기본 클래스이거나 Observable* 인터페이스를 구현하지 않는 인터페이스인 경우에는 변수가 식별되지 않습니다.

다양한 구성(예: 가로 모드 또는 세로 모드)에 대한 다양한 레이아웃 파일이 있을 때는 변수들이 결합됩니다. 이런 레이아웃 파일 간에 충돌하는 변수 정의가 없어야 합니다.

생성되는 바인딩 클래스는 설명되어 있는 변수마다 각각 setter와 getter가 있습니다. 변수는 setter가 호출될 때까지 기본 Jave 값을 취합니다. 즉, 참조 형식에는 null, int에는 0, boolean에는 false 값을 취합니다.

필요에 따라 바인딩 식에 사용하기 위해 context로 명명된 별도의 변수가 생성됩니다. context의 값은 루트 View의 getContext()에서 가져온 Context입니다. context 변수는 그 이름을 가진 명시적 변수 선언으로 재정의됩니다.

사용자 지정 바인딩 클래스 이름

기본적으로, Binding 클래스는 레이아웃 파일의 이름을 바탕으로 생성되는데, 대문자로 시작하고 밑줄( _ )은 제거되고 다음 문자를 대문자로 바꾼 다음 "Binding"이 접미사로 붙습니다. 이 클래스는 모듈 패키지 아래의 데이터 바인딩 패키지에 배치됩니다. 예를 들어, 레이아웃 파일 contact_item.xmlContactItemBinding을 생성합니다. 모듈 패키지가 com.example.my.app인 경우에는 com.example.my.app.databinding에 배치됩니다.

data 요소의 class 특성을 조정하여 바인딩 클래스의 이름을 바꾸거나 바인딩 클래스를 다른 패키지에 배치할 수도 있습니다. 예를 들어 다음과 같이 합니다.

<data class="ContactItem">
    ...
</data>

그러면 모듈 패키지의 데이터 바인딩 패키지에서 바인딩 클래스가 ContactItem으로 생성됩니다. 모듈 패키지 내에서 다른 패키지에 클래스를 생성해야 할 경우 다음과 같이 "." 접두사를 붙이면 됩니다.

<data class=".ContactItem">
    ...
</data>

이 경우 ContactItem이 모듈 패키지 내에 직접 생성됩니다. 전체 패키지가 제공되는 경우에는 어떤 패키지든 사용될 수 있습니다.

<data class="com.example.ContactItem">
    ...
</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>

name.xmlcontact.xml 레이아웃 파일에 모두 user 변수가 있어야 합니다.

데이터 바인딩은 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>
   <merge>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

식 언어

공통적인 특징

식 언어는 Java 식과 매우 흡사해 보입니다. 다음 사항은 Java 식과 똑같습니다.

  • 수학 + - / * %
  • 문자열 연결 +
  • 논리 && ||
  • 이항 & | ^
  • 단항 + - ! ~
  • 시프트 >> >>> <<
  • 비교 == > < >= <=
  • instanceof
  • 그룹화 ()
  • 리터럴 - 문자, 문자열, 숫자, null
  • 형변환
  • 메서드 호출
  • 필드 액세스
  • 배열 액세스 []
  • 삼항 연산자 ?:

예:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

연산 누락

Java에서 사용할 수 있는 식 구문에서 몇 가지 연산이 누락되었습니다.

  • this
  • super
  • new
  • 명시적 일반 호출

null 병합 연산자

null 병합 연산자(??)는 왼쪽 피연산자가 null이 아니면 왼쪽 피연산자를 선택하고, null이면 오른쪽 피연산자를 선택합니다.

android:text="@{user.displayName ?? user.lastName}"

이는 기능적으로는 다음과 동일합니다.

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

속성 참조

첫 번째 속성 참조는 앞서 나온 첫 번째 데이터 바인딩 식 작성에서 이미 설명했는데, 이는 단축 형식의 JavaBean 참조입니다. 식이 클래스의 한 속성을 참조할 때, 필드, getter, ObservableField에 똑같은 형식이 사용됩니다.

android:text="@{user.lastName}"

NullPointerException 방지

생성되는 데이터 바인딩 코드는 자동으로 null 여부를 검사하여 null 포인터 예외를 피합니다. 예를 들어, 식 @{user.name}에서 user가 null인 경우 user.name에는 기본값(null)이 할당됩니다. user.age를 참조할 경우(여기서 age는 int) 기본값은 0으로 설정됩니다.

컬렉션

편의상 [] 연산자를 사용하여 배열, 목록, 희소 목록, 맵 등의 공통 컬렉션에 액세스할 수 있습니다.

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String&gt;"/>
    <variable name="sparse" type="SparseArray&lt;String&gt;"/>
    <variable name="map" type="Map&lt;String, String&gt;"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

문자열 리터럴

다음과 같이 특성 값을 작은따옴표로 묶어서 사용하면 식에서 큰따옴표를 사용하기 쉽습니다.

android:text='@{map["firstName"]}'

특성 값을 큰따옴표로 묶을 수도 있습니다. 그럴 경우 문자열 리터럴에는 ' 또는 역따옴표(`) 중 하나를 사용해야 합니다.

android:text="@{map[`firstName`}"
android:text="@{map['firstName']}"

리소스

다음과 같이 일반적인 구문을 사용하는 식의 일부로 리소스에 액세스할 수 있습니다.

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

다음과 같이 매개변수를 제공하여 형식 문자열과 복수형을 평가할 수 있습니다.

android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"

복수형이 여러 매개변수를 취할 때는 모든 매개변수가 전달되어야 합니다.


  Have an orange
  Have %d oranges

android:text="@{@plurals/orange(orangeCount, orangeCount)}"

일부 리소스에는 명시적 형식 평가가 필요합니다.

형식 일반적인 참조 식 참조
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

데이터 객체

데이터 바인딩에 임의의 POJO(plain old Java object)를 사용할 수 있지만, POJO를 수정하더라도 UI가 업데이트되지는 않습니다. 데이터가 변경될 때 이를 알려주는 기능을 데이터 객체에 부여하면 데이터 바인딩의 진정한 강점을 활용할 수 있습니다. Observable 객체, Observable 필드, Observable 컬렉션이라는 세 가지 다른 데이터 변경 알림 메커니즘이 있습니다.

이러한 Observable 데이터 객체 중 하나가 UI에 바인딩되어 있고 데이터 객체의 속성이 변경되면 UI가 자동으로 업데이트됩니다.

Observable 객체

android.databinding.Observable 인터페이스를 구현하는 클래스를 사용하면 바인딩이 바인딩된 객체에 단일 리스너를 연결하여 그 객체에 대한 모든 속성의 변경 사항을 수신할 수 있습니다.

android.databinding.Observable 인터페이스에는 리스너를 추가하고 제거하는 메커니즘이 있지만, 알림은 개발자의 선택에 따라 결정됩니다. 더 쉽게 개발할 수 있도록, 기본 클래스 android.databinding.BaseObservable을 만들어 리스너 등록 메커니즘을 구현했습니다. 속성이 변할 때 이를 알릴 책임은 여전히 데이터 클래스 구현자에게 있습니다. getter에 android.databinding.Bindable 주석을 할당하고 setter에서 이를 알림으로써 속성 변경을 알릴 수 있습니다.

private static class User extends BaseObservable {
   private String firstName;
   private String lastName;
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

android.databinding.Bindable 주석은 컴파일 중에 BR 클래스 파일에 항목을 생성합니다. BR 클래스 파일은 모듈 패키지에 생성됩니다. 데이터 클래스에 대한 기본 클래스를 변경할 수 없는 경우, 리스너를 효율적으로 저장하고 알리기에 편리한 android.databinding.PropertyChangeRegistry를 사용하여 android.databinding.Observable 인터페이스를 구현할 수 있습니다.

ObservableField

android.databinding.Observable 클래스를 생성하려면 약간의 작업이 필요하므로, 시간을 절약하고 싶거나 속성이 거의 없는 개발자는 android.databinding.ObservableField와 그 형제들인 android.databinding.ObservableBoolean, android.databinding.ObservableByte, android.databinding.ObservableChar, android.databinding.ObservableShort, android.databinding.ObservableInt, android.databinding.ObservableLong, android.databinding.ObservableFloat, android.databinding.ObservableDouble, android.databinding.ObservableParcelable을 사용할 수 있습니다. ObservableFields는 단일 필드를 가진, 자체 포함 방식의 Observable 객체입니다. 원시 버전은 액세스 작업 중에 boxing과 unboxing을 방지합니다. 이를 사용하려면 다음과 같이 데이터 클래스에 public final 필드를 생성합니다.

private static class User {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

다 되었습니다! 값에 액세스하려면 set 및 get 접근자 메서드를 사용합니다.

user.firstName.set("Google");
int age = user.age.get();

Observable 컬렉션

일부 애플리케이션에서는 데이터 유지를 위해 더욱 동적인 구조체를 사용합니다. Observable 컬렉션을 사용하면 이러한 데이터 객체에 키 입력 방식으로 액세스할 수 있습니다. 키가 String과 같은 참조 형식일 때는 android.databinding.ObservableArrayMap이 유용합니다.

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

레이아웃에서 다음과 같이 String 키를 통해 맵에 액세스할 수도 있습니다.

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap&lt;String, Object&gt;"/>
</data>
…
<TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

키가 정수일 때는 android.databinding.ObservableArrayList가 유용합니다.

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);

레이아웃에서는 인덱스를 통해 목록에 액세스할 수 있습니다.

<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList&lt;Object&gt;"/>
</data>
…
<TextView
   android:text='@{user[Fields.LAST_NAME]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

생성되는 바인딩

생성되는 바인딩 클래스는 레이아웃 내에서 레이아웃 변수를 View와 연결합니다. 앞서 설명한 바와 같이, Binding의 이름과 패키지는 사용자 지정할 수 있습니다. 생성되는 바인딩 클래스는 모두 android.databinding.ViewDataBinding을 확장합니다.

생성

레이아웃 내에서 식을 이용해 View에 바인딩하기 전에 View 계층 구조가 방해되지 않도록 하기 위해 확장 직후에 바인딩을 생성해야 합니다. 레이아웃에 바인딩하는 몇 가지 방법이 있습니다. 가장 일반적인 방법은 Binding 클래스에 정적 메서드를 사용하는 것입니다. inflate 메서드는 View 계층 구조를 확장하고 그 모두를 한 번에 바인딩합니다. 다음과 같이 LayoutInflater만 취하는 간단한 버전과 ViewGroup도 취하는 버전이 있습니다.

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);

다른 메커니즘으로 레이아웃을 확장시킨 경우에는 따로 바인딩될 수도 있습니다.

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

바인딩을 미리 알 수 없을 때도 있습니다. 그럴 때는 android.databinding.DataBindingUtil 클래스를 사용하여 바인딩을 생성할 수 있습니다.

ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

ID가 있는 뷰

레이아웃에서 ID가 있는 각각의 View에 대해 공용 최종 필드가 생성됩니다. 바인딩은 View 계층 구조에서 한 번 전달하여 ID가 있는 View를 추출합니다. 이 메커니즘이 여러 View에 대해 findViewById를 호출하는 것보다 더 빠를 수 있습니다. 예를 들면 다음과 같습니다.

<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}"
   android:id="@+id/firstName"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
  android:id="@+id/lastName"/>
   </LinearLayout>
</layout>

위 코드는 다음 코드로 바인딩 클래스를 생성합니다.

public final TextView firstName;
public final TextView lastName;

ID는 데이터 바인딩이 없으면 거의 필요가 없지만, 여전히 코드에서 View에 액세스해야 할 경우가 있습니다.

변수

각 변수에는 접근자 메서드가 주어집니다.

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

위 코드는 다음과 같이 바인딩에 setter와 getter를 생성합니다.

public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);

ViewStub

ViewStub은 일반적인 View와는 약간 다릅니다. ViewStub은 보이지 않게 시작되어 마침내 보이게 되거나 명시적으로 확장하라는 명령이 있을 때 다른 레이아웃을 확장하여 레이아웃에서 스스로를 대체합니다.

ViewStub은 본질적으로 View 계층 구조에서 사라지기 때문에, 컬렉션을 허용하기 위해 바인딩 객체의 View 역시 사라져야 합니다. View는 최종적인 것이므로, android.databinding.ViewStubProxy 객체가 ViewStub을 대신하여 ViewStub이 존재할 뿐 아니라 ViewStub 확장 시 확장된 View 계층 구조에 액세스하기도 할 때 개발자에게 ViewStub에 대한 액세스 권한을 부여합니다.

다른 레이아웃을 확장할 때는 새 레이아웃에 대해 바인딩이 설정되어 있어야 합니다. 따라서 ViewStubProxyViewStubViewStub.OnInflateListener를 수신하고 바로 그 때 바인딩을 설정해야 합니다. ViewStubProxy는 하나만 존재할 수 있으므로, 개발자는 ViewStubProxy에 대한 OnInflateListener를 설정하여 바인딩 설정 후에 호출하도록 할 수 있습니다.

고급 바인딩

동적 변수

가끔 특정 바인딩 클래스를 알 수 없을 때가 있습니다. 예를 들어, 임의의 레이아웃에 대해 작동하는 RecyclerView.Adapter가 특정 바인딩 클래스를 알지 못하는 경우입니다. 그래도 onBindViewHolder(VH, int) 중에 바인딩 값을 할당해야 합니다.

아래 예시에서는 RecyclerView가 바인딩하는 모든 레이아웃에 "item" 변수가 있습니다. BindingHolder에는 android.databinding.ViewDataBinding 베이스를 반환하는 getBinding 메서드가 있습니다.

public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}

즉각적인 바인딩

변수나 Observable이 변경되면 바인딩이 다음 프레임 전에 변경되도록 예약됩니다. 하지만 바인딩을 즉시 실행해야 할 때가 있습니다. 이럴 때 강제로 실행하려면 ViewDataBinding#executePendingBindings() 메서드를 사용하세요.

백그라운드 스레드

데이터 모델이 컬렉션이 아닌 한, 백그라운드 스레드에서 데이터 모델을 변경할 수 있습니다. 데이터 바인딩은 동시 실행 문제를 방지하기 위해 평가하는 동안 각각의 변수/필드를 지역 범위로 설정합니다.

특성 Setter

바인딩된 값이 바뀔 때마다, 생성되는 바인딩 클래스는 바인딩 식과 함께 View에 대한 setter 메서드를 호출해야 합니다. 데이터 바인딩 프레임워크에는 값을 설정하기 위해 호출할 메서드를 사용자 지정하는 방법이 있습니다.

자동 Setter

특성의 경우, 데이터 바인딩은 메서드 setAttribute 찾기를 시도합니다. 특성의 네임스페이스는 중요하지 않고, 특성 이름 자체만 찾으면 됩니다.

예를 들어, TextView의 특성 android:text와 연관된 식은 setText(String)을 찾습니다. 식에서 int가 반환되는 경우 데이터 바인딩은 setText(int) 메서드를 검색합니다. 식이 올바른 형식을 반환하도록 주의하고, 필요할 경우에는 형변환하세요. 참고로, 주어진 이름을 가진 특성이 존재하지 않더라도 데이터 바인딩은 작동합니다. 그러면 데이터 바인딩을 사용하여 어떤 setter에 대한 특성도 쉽게 "생성"할 수 있습니다. 예를 들어, DrawerLayout에 특성이 없지만 setter는 많이 있다고 가정해 봅시다. 자동 setter를 사용하면 이런 setter 중 하나를 사용할 수 있습니다.

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

이름이 바뀐 Setter

setter의 이름이 일치하지 않는 특성도 있습니다. 이런 메서드의 경우, android.databinding.BindingMethods 주석을 통해 특성을 setter와 연결할 수 있습니다. 메서드는 클래스와 연결되어야 하며 이름이 바뀐 메서드마다 android.databinding.BindingMethod 주석이 하나씩 포함됩니다. 예를 들어, android:tint 특성은 실제로는 setTint가 아니라 setImageTintList(ColorStateList)와 연결됩니다.

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

Android 프레임워크 특성이 이미 구현되었으므로, 개발자가 setter의 이름을 바꿔야 할 일은 거의 없을 것입니다.

사용자 지정 Setter

일부 특성은 사용자 지정 바인딩 로직이 필요합니다. 예를 들어, android:paddingLeft 특성에 대해 연결된 setter가 없습니다. 그 대신, setPadding(left, top, right, bottom)이 존재합니다. 개발자는 android.databinding.BindingAdapter 주석이 있는 정적 바인딩 어댑터 메서드를 사용하여 특성의 setter를 호출하는 방법을 사용자 지정할 수 있습니다.

Android 특성에는 이미 BindingAdapter가 생성되어 있습니다. 예를 들어, 다음은 paddingLeft에 대한 것입니다.

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

바인딩 어댑터는 다른 형식의 사용자 지정에 유용합니다. 예를 들어, 이미지를 로드하기 위해 오프스레드 상태로 맞춤 로더를 호출할 수 있습니다.

개발자가 만든 바인딩 어댑터는 충돌이 있을 경우 데이터 바인딩 기본 어댑터를 무시합니다.

여러 매개변수를 받는 어댑터를 사용할 수도 있습니다.

@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>

imageUrlerror가 둘 다 ImageView에 사용되고 imageUrl이 문자열이고 error가 드로어블인 경우에 이 어댑터가 호출됩니다.

  • 맞춤 네임스페이스는 일치 확인 중에 무시됩니다.
  • Android 네임스페이스용 어댑터를 작성할 수도 있습니다.

바인딩 어댑터 메서드는 선택적으로 핸들러의 기존 값을 취할 수도 있습니다. 기존 값과 새 값을 취하는 메서드는 특성의 모든 기존 값을 먼저 가진 후 새 값을 가져야 합니다.

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
   if (oldPadding != newPadding) {
       view.setPadding(newPadding,
                       view.getPaddingTop(),
                       view.getPaddingRight(),
                       view.getPaddingBottom());
   }
}

이벤트 핸들러는 한 개의 추상 메서드를 가진 추상 클래스나 인터페이스와 함께 사용할 수 있을 뿐입니다. 예를 들면 다음과 같습니다.

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue);
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue);
        }
    }
}

리스너에 여러 개의 메서드가 있을 때는 여러 개의 리스너로 분할해야 합니다. 예를 들어, View.OnAttachStateChangeListener에는 onViewAttachedToWindow()onViewDetachedFromWindow()의 두 메서드가 있습니다. 그러면 이들 메서드의 특성과 핸들러를 구분하기 위해 두 개의 인터페이스를 만들어야 합니다.

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
    void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
    void onViewAttachedToWindow(View v);
}

한 리스너를 변경하면 다른 리스너에도 영향을 미치기 때문에 세 가지의 각기 다른 바인딩 어댑터가 있어야 합니다. 즉, 각각의 특성을 위한 바인딩 어댑터 하나와 두 리스너 모두에 하나씩(둘 모두 설정되어 있어야 함) 있어야 합니다.

@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
    setListener(view, null, attached);
}

@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
    setListener(view, detached, null);
}

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
        final OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        final OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }
        final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
                newListener, R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

View가 View.OnAttachStateChangeListener에 대한 set 메서드를 사용하는 대신 리스너에 대한 add와 remove를 사용하기 때문에, 위 예시는 일반적인 것보다 약간 더 복잡합니다. android.databinding.adapters.ListenerUtil 클래스는 Binding Adaper에서 이전의 리스너를 제거할 수 있도록 이들을 계속 추적하는 데 도움이 됩니다.

인터페이스 OnViewDetachedFromWindowOnViewAttachedToWindow@TargetApi(VERSION_CODES.HONEYCOMB_MR1) 주석을 추가하면 데이터 바인딩 코드 생성기가 addOnAttachStateChangeListener(View.OnAttachStateChangeListener)에 의해 지원되는 같은 버전인 Honeycomb MR1과 새 기기에서 실행 시 리스너만 생성되어야 한다는 점을 인식하게 됩니다.

변환기

객체 변환

바인딩 식에서 Object가 반환되면 자동 setter, 이름이 바뀐 setter, 맞춤 setter 중에서 setter가 선택됩니다. Object는 선택된 setter의 매개변수 형식으로 형변환됩니다.

이는 ObservableMaps를 사용하여 데이터를 유지하는 경우에 편리합니다. 예를 들면 다음과 같습니다.

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

userMap은 Object를 반환하고 그 Object는 setter setText(CharSequence)에서 찾은 매개변수 형식으로 자동 형변환됩니다. 매개변수 형식에 대한 혼동이 있을 수 있을 때는 개발자가 식을 통해 형변환해야 합니다.

사용자 지정 변환

때때로 특정 형식 간에 자동으로 변환이 이루어져야 합니다. 배경을 설정할 때를 예로 들면 다음과 같습니다.

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

여기서 배경은 Drawable을 취하지만 색은 정수 형식입니다. Drawable이 실행되고 정수가 반환될 때마다 intColorDrawable로 변환되어야 합니다. 이 변환은 BindingConversion 주석과 함께 정적 메서드를 사용하여 수행됩니다.

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

참고로, 변환은 setter 레벨에서만 이루어지므로 다음과 같이 형식을 혼합할 수 없습니다.

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

Android Studio의 데이터 바인딩 지원

Android Studio는 데이터 바인딩 코드를 위한 다양한 코드 편집 기능을 지원합니다. 예를 들어, 데이터 바인딩 식에 대해 다음과 같은 기능을 지원합니다.

  • 구문 강조표시
  • 식 언어 구문 오류의 플래그 지정
  • XML 코드 완성
  • (선언 탐색 등의) 탐색빠른 문서화를 포함한 참조

참고: 배열과 제네릭 형식(예: android.databinding.Observable 클래스)은 오류가 없을 때 오류를 표시할 수도 있습니다.

Preview 창에 데이터 바인딩 식의 기본값이 표시됩니다(제공되는 경우). 레이아웃 XML 파일의 한 예시에서 발췌한 다음 예시 코드에서, Preview 창은 TextViewPLACEHOLDER 기본 텍스트 값을 표시합니다.

<TextView android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{user.firstName, default=PLACEHOLDER}"/>

프로젝트 디자인 단계 중에 기본값을 표시해야 할 경우, Designtime 레이아웃 특성에 설명되어 있는 것처럼 기본 식 값 대신 도구 특성을 사용할 수도 있습니다.