【Android】Android の Data Binding でできること(基本編)
2018/12/01 追記
- シンタックスハイライトを適用しました。
- こんな場末の技術ブログを見るぐらいなら公式のドキュメントをちゃんと読んだほうがいいと思います。
- 記事公開当時は大した情報が記載されていませんでしたが、今はかなり充実しています。
前書き
最近(と言っても 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; } }
<!--?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>
なんか別の言語で 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.gradle
にdataBinding { 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 <string> firstName = new ObservableField<string>(); private final ObservableField<string> lastName = new ObservableField<string>(); public User(String firstName, String lastName) { this.firstName.set(firstName); this.lastName.set(lastName); } public ObservableField<string> getFirstName() { return this.firstName; } public ObservableField<string> 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 なMap
やList
の実装方法なんかもありますが、「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} /> <!-- android:onLongClick --> </LinearLayout> </layout>
@BindingMethod
と@BindingAdapter
独自のイベントを設定するには@BindingMethod
と@BindingAdapter
と言う黒魔術アノテーションを駆使する必要があります。
と言うか、先ほどの例で挙げたandroid:onclick
もandroid:onLongClick
も@BindingMethod
でやってるだけです。この 2 つのアノテーションは既存のView
だろうがなんだろうが attr を拡張することができます。
公式ドキュメントを読むと@BindingMethod
はsetter をリネームしたい時、@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:imageUrl
とapp: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#VISIBLE
とView#GONE
の切り替えなんかは相当楽になりますね。
また、(pure な)Java にはまだないNull 合体演算子がサポートされています。
android:text="@{user.displayName ?? user.lastName}"
<!-- android:text="@{user.displayName != null ? 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
だとしたら、MainActivityBinding
はcom.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.xml
とcontact.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.setBackground
はDrawable
しか受け付けないんですが、color
のリソース ID(int
)を渡したいとします。
当然このままでは怒られるので、ColorDrawable
に変換する static メソッドを作り、@BindingConversion
を指定します。
@BindingConversion public static ColorDrawable convertColorToDrawable(int color) { return new ColorDrawable(color); }
これでandroid:background
にint
が渡されたとしてもColorDrawable
に変換してくれます。
何も知らないと非常に混乱しそうなので、用量、用法を守ってお使いください…。と言うか、@BindingAdapter
と違って適用する attr を制限できないっぽいのでめちゃめちゃ危険です。使わない方がいいかもしれません。
ちなみに、Drawable
とint
のどちらかが返されるみたいなパターンは普通にダメです。
<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においておきました。それぞれ、こんな感じです。
- Data Binding の導入
- とりあえず適当にバインディングしてみる
- Observable なプロパティとイベントのバインディング
BaseObservable
を継承したパターン@MethodAdapter
で遊んでみる
参考
ぶっちゃけ公式ドキュメントが一番わかりやすかったです。