読者です 読者をやめる 読者になる 読者になる

【Android】AndroidのData Bindingでできること(基本編)

前書き

最近(と言っても1~2ヶ月前の話ですが)、訳あって携帯を機種変更し、Android 5.1.1の端末を手に入れました。

いい機会なので開発環境も一新し、Eclipse + ADTとか言うファッキンな環境を捨ててAndroid Studio + Gradle + Kotlinに切り替えました。

IntelliJのlombokプラグイン@ExtensionMethodに対応していればKotlinは恐らく触りもしなかったんですが、Scalaをちょっと触っていたこともあってか、中々快適に開発できています。普段の業務もこれで書きたいぐらいです。

Kotlinの話はいずれするかもしれないし、しないかもしれませんが、今回はAndroid Studio 1.3からサポートされ始めたData Bindingの話をします。

Data Bindingとは?

レイアウト用のXML/res/layout/内のやつ)にデータを定義することで、そのデータのプロパティに直接アクセスできるようになりました。

以下のコードを見ればなんとなくわかると思います。(コードはドキュメントのサンプルそのままです。)

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

なんか別の言語でGUIのアプリを作るときも似たようなやつを必死に書いた気がするんですが、きっと気のせいでしょう。と言うか、XAMLよりもシンプル(当社比)でいい感じです。Javaだとわざわざgetterを作らなきゃいけないからそこで帳消しですよ。

実際にデータをセットする時はこんな感じに呼び出します。

main_activity.xmlであれば、MainActivityBindingと言うクラスが自動で生成されます。

public class MainActivity extends Activity {
    @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);
    }
}

プラグインの導入

Android Plugin for Gradleの1.5.0-alpha1からプラグインが統合されたため、基本的にはmoduleレベルのbuild.gradledataBinding { enabled = true }を追加するだけでOKです。

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

何らかの事情で古いものを使っている人は別途プラグインを入れる必要があります。まずはrootのbuild.gradle内のdependenciesにDataBinderを追加します。

最新版がどれかわからない、って人はここにあるので適当に見繕ってきてください。

buildscript {
    repositories {
        jcenter()
        mavenCentral()
    }
    dependencies {
        // ...
        classpath "com.android.databinding:dataBinder:1.0-rc4"
    }
}

次にmoduleレベルでのbuild.gradleに設定を入れていきます。と言っても、apply plugin: 'com.android.databinding'を突っ込むだけです。

apply plugin: 'com.android.databinding'

android {
    // ...

データオブジェクト

プロパティの定義

Javaのフィールドをプロパティとして呼び出すためには、以下の二条件のうちどちらか一つを満たしていればOKです。

  • publicなフィールドである
  • getterが定義されている

具体的にはこんな感じですね。

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}
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;
   }
}

データ変更の検知

上記の例だとデータを変更しても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);
   // firstNameの値を変更してもTextViewの表示は変わらない
   user.setFirstName("Hoge")
}

データが変わったら自動でViewの表示も変わって欲しい人もいると思います。そんな人のためにいくつかの手段が用意されています。

まずはObservableFieldsを使用する例です。

public class User {
   private final ObservableField firstName = new ObservableField();
   private final ObservableField lastName = new ObservableField();

   public User(String firstName, String lastName) {
       this.firstName.set(firstName);
       this.lastName.set(lastName);
   }
   public ObservableField getFirstName() {
       return this.firstName;
   }
   public ObservableField getLastName() {
       return this.lastName;
   }
   
}
@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);
   // firstNameの値を変更するとTextViewの表示も変わる
   user.getFirstName.set("Hoge");
}

うーん、このダサさ。ちなみにプリミティブ型のためにObservableBooleanとかObservableIntなんてのがそれぞれも用意されてます。一秒でも早くJavaが絶滅してくれないかなと思ったことでしょう。

他にもObservableなMapListの実装方法なんかもありますが、「XML実体参照を書かないといけないの、流石にダサすぎでは?」ぐらいしか言うことないので、必要なら読んどいてください。

もうちょっとスマートな方法としてBaseObservableを継承する方法があります。

public class User extends BaseObservable {
   private final String firstName;
   private final String lastName;

   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
   
   @Bindable // 変更を通知したいプロパティには@Bindbleをつける
   public String getFirstName() {
       return this.firstName;
   }
   
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       // @Bindableがついているプロパティは
       // BR.プロパティ名と言うフィールドが自動で生成される
       notifyPropertyChanged(BR.firstName); 
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}
@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);
   // firstNameの値を変更するとTextViewの表示も変わる
   user.setFirstName("Hoge");
}

implementsじゃなくてextendsなのがキツいところですね。

本当はBaseObservableではなくObservableを実装すればいいみたいなんですが、当然通知の仕組みは自力で実装する必要があります。馬鹿にしていらっしゃるのかな?(敬語)

でもまぁ、ソースを読む限り大した実装ではないので、必要なら自力で実装しちゃってもいいかもです。

また、どうしてもsetterに追記が必要なので、lombokを使っている人からすると二倍しんどいです。勘弁して欲しいですね。

イベントのバインド

android:onClickのように何らかのListenerを設定できる場所にはイベントをバインドすることができます。

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
   
   public void showFullName(View v) {
       Toast.makeText(v.getContext(), this.firstName + " " + this.lastName, Toast.LENGTH_SHORT).show();
   }
}
<?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}"/>
       <Button android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="show fullname"
           android:onclick=@{user.showFullName} />
   </LinearLayout>
</layout>

また、本来なら定義されていないはずのandroid:onLongClickと言うattrにイベントをバインドすることもできます。理由は後述。

<?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}"/>
       <Button android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="show fullname"
           android:onLongClick=@{user.showFullName} />
   </LinearLayout>
</layout>

@BindingMethod@BindingAdapter

独自のイベントを設定するには@BindingMethod@BindingAdapterと言う黒魔術アノテーションを駆使する必要があります。

と言うか、先ほどの例で挙げたandroid:onclickandroid:onLongClick@BindingMethodでやってるだけです。この2つのアノテーションは既存のViewだろうがなんだろうがattrを拡張することができます。

公式ドキュメントを読むと@BindingMethodsetterをリネームしたい時@BindingAdapter独自のsetterを定義したい時に使うといいよと紹介されています。

つまり、目的のViewが既に対応するようなメソッドを持っている時は@BindingMethodを、完全に自分で定義(拡張)したい時は@BindingAdapterを使ってね、ぐらいのイメージでしょうか。

@BindingMethodの使用例

せっかくだから先ほどの例がどのように定義されているのか実際のソースを見てみましょう。

と言っても関係あるとこだけしか抜き出しませんが。長いしね。

@BindingMethods({
        @BindingMethod(type = View.class, attribute = "android:onClick", method = "setOnClickListener"),
        @BindingMethod(type = View.class, attribute = "android:onLongClick", method = "setOnLongClickListener"),
})
public class ViewBindingAdapter {

上記の記述により、Viewを継承しているクラスのandroid:onClickに何らかのメソッドがバインドされた場合、View.setOnClickListenerへ処理を移譲することができます。中々クールですね。

ちなみに@BindingMethods@Target({ElementType.TYPE})で定義されていますが、多分、本当はどこでもよかったんでしょうね…。

@BindingAdapterの使用例

例えば、ImageViewに対してGlideをつかって画像をセットしたい時とかあると思います。

(公式ドキュメントはなぜかPicassoを例にしていますが、あえて競合他社のライブラリを例にした理由は謎です。)

面倒なのでModelは超単純にしましょう。

public class Model {
    public final String imageUrl;
    public Model(String imageUrl) {
        this.imageUrl = imageUrl;
    }
}

こんな形でバインドできたらありがたいです。名前空間androidじゃなくてres-autoなので注意しましょう。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="m" type="com.example.Model"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <ImageView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           app:imageUrl="@{m.imageUrl}" />
   </LinearLayout>
</layout>

わざわざImageViewを継承して独自のattrを作らずともBindingAdapterを使うことで簡単に実現できます。

public final class ImageViewExtensions {
    @BindingAdapter({"bind:imageUrl"})
    public static void loadImage(ImageView v, String imageUrl) {
        Glide.with(v.getContext()).load(imageUrl).into(v);
    }
}

「画像を読み込めたらそれを表示してほしいけど、失敗したらdrawableの画像を表示したい」なんて要件も当然あると思います。

そんな時は@BindingAdapter複数の引数を渡せばOKです。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="m" type="com.example.Model"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <ImageView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           app:imageUrl="@{m.imageUrl}"
           app:error="@{@drawable/error}" />
   </LinearLayout>
</layout>
public final class ImageViewExtensions {
    @BindingAdapter({"bind:imageUrl", "bind:error"})
    public static void loadImage(ImageView v, String imageUrl, Drawable error) {
        Glide.with(v.getContext()).load(imageUrl).error(error).into(v);
    }
}

ただし、上記の場合だと必ずapp:imageUrlapp:errorの両者を指定する必要があります。

app:errorを指定しないこともあるかも…という場合は、@BindingAdapterを指定するメソッドを2つ用意するしかありません。(ダサい)

public final class ImageViewExtensions {
    @BindingAdapter({"bind:imageUrl"})
    public static void loadImage(ImageView v, String imageUrl) {
        Glide.with(v.getContext()).load(imageUrl).into(v);
    }
    @BindingAdapter({"bind:imageUrl", "bind:error"})
    public static void loadImage(ImageView v, String imageUrl, Drawable error) {
        Glide.with(v.getContext()).load(imageUrl).error(error).into(v);
    }
}

レイアウトXMLの細かい文法

前述した内容だけでもある程度使えますが、レイアウトXMLでもっと色んなことができるようになってるのでガンガン活用しましょう。

演算子

ちょっとした計算やbool値の判定なんかもレイアウトXML内でできるようになりました。

具体的に何ができるかは公式ドキュメントを読んでください。5分もあれば読めますよ。

以下、基本的なコード例です。公式のやつそのまんまですが。

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

View#VISIBLEView#GONEの切り替えなんかは相当楽になりますね。

また、(pureな)JavaにはまだないNull合体演算子がサポートされています。

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

Import

<data>内にImport文を書くことが可能です。

で?と思うかもしれませんが、Android SDKフレームワーク内のあれこれを使うときとかに便利です。

<?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="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:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
   </LinearLayout>
</layout>

packageは違うけど同名のクラスを使う場合はaliasを設定できます。

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

バインディングクラスのパッケージ / 名称変更

バインディングクラスってなんじゃい、って話ですが、例えばmain_activity.xmlであれば、MainActivityBindingと言うクラスが自動で生成されます。このMainActivityBindingバインディングクラスです。

もしrootパッケージがcom.example.my.appだとしたら、MainActivityBindingcom.example.my.app.databindingに配置されます。

何らかの理由があってこのデフォルトで生成されるクラスの名前や、生成されるパッケージを変更したい場合はdata要素のattributeとしてclassを指定してあげればOKです。

クラス名を変えるだけならこんな感じです。

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

パッケージも変更したいのであれば、フルパスで指定してあげましょう。

<data class="com.example.MainActivityViewHolder">
    ...
</data>

rootパッケージ直下に置きたい場合は先頭に.をつけてあげればOKです。

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

Include先へのバインディング

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>

当然と言えば当然ですが、Include先のレイアウトファイルでもちゃんとバインディングの設定をしておく必要があります。

上記の例で言えば、name.xmlcontact.xml内でuserと言うvariableがなければ叱られます。

また、<merge>要素直下の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なのでこれはダメ -->
   <merge>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

暗黙の型変換

これまた黒魔術っぽいんですが…。本来なら適切でない型がバインドされても、@BindingConversionが指定されているメソッドに移譲して型変換することができます。

ちょっと何言ってるかわかんないですよね。例えばこんな感じです。

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

View.setBackgroundDrawableしか受け付けないんですが、colorのリソースID(int)を渡したいとします。

当然このままでは怒られるので、ColorDrawableに変換するstaticメソッドを作り、@BindingConversionを指定します。

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

これでandroid:backgroundintが渡されたとしてもColorDrawableに変換してくれます。

何も知らないと非常に混乱しそうなので、用量、用法を守ってお使いください…。と言うか、@BindingAdapterと違って適用するattrを制限できないっぽいのでめちゃめちゃ危険です。使わない方がいいかもしれません。

ちなみに、Drawableintのどちらかが返されるみたいなパターンは普通にダメです。

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

とは言え、staticメソッドは普通に呼べるのでそっちで回避できます。

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

…あれ?じゃあほんとにこれいらなくね?

まとめ

久しぶりに濃厚な記事になってしまいました。

基本編と銘打ってはいますが、応用編を書く予定とネタは今のところありません。

自分でも色々いじってみたテスト用プロジェクトをGitHubにおいておきました。それぞれ、こんな感じです。

  1. Data Bindingの導入
  2. とりあえず適当にバインディングしてみる
  3. Observableなプロパティとイベントのバインディング
  4. BaseObservableを継承したパターン
  5. @MethodAdapterで遊んでみる

参考

ぶっちゃけ公式ドキュメントが一番わかりやすかったです。