はじめに
基本的なことは省略してます。
設定
android {
…
dataBinding {
enabled = true
}
}
Android Studioおよびgradle pluginの2.2.0を利用している人は、悲しいバグを踏むので2.2.1にしましょう。
制限
現状では、JackコンパイラでDataBindingは使えません。
そのため、Java8の機能が使いたい場合はライブラリで補完しましょう。
| 機能 | ライブラリ |
|---|---|
| Lambda | Retrolambda |
| Stream API | Lightweight-Stream-API |
制約
- Javaの予約語に準拠
-
contextは変数名として使えません - Contextインスタンスが参照できます
- マルチバイト文字は使えません
- マルチバイト文字のメソッドなどを呼び出しているとビルドに失敗します
DataBindingの特徴
とりあえずこのへんに触れます。
- Null安全
- メソッドアクセスの効率化
- mergeタグ不要
- DataBindingの連係
- カスタム属性
Null安全
DataBindingでは、レイアウトに書いたコードについてはNullPointerExceptionにならないという特性があります。
特に記述されていませんが、これには配列やListのArrayIndexOutOfBoundsExceptionなども含まれています。
<layout>
<data>
<variable
name="person"
type="foo.bar.Person"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{person.name}"/>
</layout>
この場合、コード側で
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setPerson(null);
と、nullをセットされていても、ぬるぽにはなりません。
android:text="@{person.name.firstName}"
この場合の、nameがnullの場合も同様です。
配列外アクセスの場合も同様に安全に処理してくれます。(実装上はnullであると判定される)
DataBindingのシンタックスシュガーとして、**A ?? B**というものがあります。
これは左辺Aがnullの場合に右辺Bを使うという表現ですが、これは左辺が配列外アクセスであっても同様に適用されます。
メソッドアクセスの効率化
DataBindingでは1回の画面更新で、同じ処理は1度しか行わないように実装されています。
これはどういうことかというと、
…
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{person.getName()}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{person.getName() + ` (` + person.getAge() + `)`}"/>
このように、2つのTextViewで同じメソッドgetName()へアクセスしている場合、生成されるコードでは以下のように使われるということです。(実際のコードとは異なります)
String name = person.getName();
int age = person.getAge();
textView1.setText(name);
textView2.setText(name + " (" + age + ")");
この特性はnullチェックにも適用され、自分で書くとめんどくさいローカル変数出しを自動で行ってくれます。
getterの特性
ちなみに、上ではperson.getName()と書きましたが、DataBindingはpublicなgetterを変数アクセスのように記述できます。
person.getName()の場合person.nameとしても有効になります。(むしろ後者しか補完されません)
では、person.getName()とperson.nameが混在するとどうなるのか。
これは、別の処理として認識されるようで、2つの変数に分割されます。ちなみにnameがpublicな変数だったとしても、person.nameはgetterを優先して呼び出されます。
mergeタグ不要
レイアウトXMLでは、mergeタグというものが存在します。
これはincludeタグやViewGroupに対してattachToRoot=trueでレイアウトをinflateしたときに、どのような階層になるかを指定するものです。(詳細は省きます)
DataBindingはトップ階層をlayoutタグにする必要があります。それに対してmergeタグはトップ階層にしか定義できません。
ではどうするのか?
答えは、mergeタグは利用できないです。
公式を見ても、Data binding does not support include as a direct child of a merge element.としか書いてありません。
この説明、非常に不親切なんですが、実際は**mergeタグを使う必要がない**のほうが正しいです。
layoutタグが非常に柔軟にできていて、includeタグでレイアウトを挿入した場合、mergeの役割を果たしてくれます。
逆にコードでinflateした場合、必要に応じてFrameLayoutで囲ってくれます。つまり、エンジニアはこの動作について気にする必要がないということです。
DataBindingの連係
includeタグ関連でもう2つ便利な動作を紹介します。
DataBindingからDataBindingを取得する
<layout>
…
<LinearLayout>
…
<include
android:id="@+id/foo_binding"
layout="@layout/layout_foo"/>
</LinearLayout>
</layout>
<layout>
<data>
<variable
name="age"
type="int"/>
</data>
…
</layout>
activity_main.xmlで、includeタグにidを振ってみます。
するとコード側では、
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
Person person = new Person("山田太郎", 25);
binding.setPerson(person);
// BindingからBindingが生える
LayoutFooBinding fooBinding = binding.fooBinding;
fooBinding.setAge(person.getAge());
このように、ActivityMainBindingからLayoutFooBindingにアクセスできるようになります。
View、DataBindingに限らず、とりあえずアクセスしたければ**idを振れば生えてくる**ということです。
逆にidを振らなければ隠蔽できます。
DataBindingからDataBindingへのデータ渡し
上の例では、コードからデータを渡していましたが、じつはこのコードはレイアウトxmlで完結させることできます。
layout_foo.xmlには、int型の変数ageが定義されています。
この場合、includeしている側のactivity_main.xmlは、includeタグの属性に以下のような記述ができます。
<layout>
…
<LinearLayout>
…
<include
layout="@layout/layout_foo"
app:age="@{person.age}"/>
</LinearLayout>
</layout>
layout_foo.xmlで定義したvariableの名前をそのまま、app:<variableName>として指定できます。
カスタム属性
dataタグに定義されている変数以外に、DataBindingでは任意の属性を外から追加することができます。
| 定義 | 属性名 | 利用可能箇所 |
|---|---|---|
variableタグ |
app:<variableName> |
includeタグ |
Viewのpublic setter
|
android:onClickListenerなど |
対象のView
|
@BindingMethodアノテーション |
任意の名前 | 定義対象のView
|
@BindingAdapterアノテーション |
任意の名前 | 引数のView
|
これらのうち、下3つについて簡単に説明します。
public setterの属性化
DataBindingではViewに定義されている全てのpublic setterに属性としてアクセスできます。(hideなものはわかりません)
例えばCustomViewを作るとき、必要なデータをsetterから受け取れるようにしたとします。
public CustomView extends View {
…
public void setPerson(Person person) {
…
invalidate();
}
}
通常は、このメソッドに対してコードからデータを渡す処理を書くと思います。
しかし、DataBindingでは、publicなsetterは属性化されるという特性を持つため、レイアウトxmlで記述することもできます。
<layout>
<data>
<variable
name="person"
type="foo.bar.Person"/>
</data>
<foo.bar.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:person="@{person}"/>
</layout>
こうすることで、1つのデータを複数のViewにそれぞれセットせずに、DataBindingに渡せばOKという一貫した処理にできます。
ただしこの場合、値がnullの場合の処理はCustomViewに委ねられるという点に注意が必要です。(実装次第でぬるぽが起こり得る)
BindingMethod
@BindingMethods及び@BindingMethodを利用することで、任意の属性を既存のメソッドにリンクさせることができます。
例として、Viewにはandroid:onClickという属性が定義されていますが、この属性名onClickに対応するメソッドsetOnClick()はViewには定義されていません。
にも関わらず、android:onClick="@{onClickListener}"は有効です。
これは、デフォルトで定義されているViewBindingAdapterに、属性名とメソッドの対応が定義されているためです。
@BindingMethods({
…
@BindingMethod(type = View.class, attribute = "android:onClick", method = "setOnClickListener")
})
public class ViewBindingAdapter {
…
}
このように、DataBindingでは、属性名とメソッド名が一致しないいくつかの属性について、デフォルトのリンク定義がされています。(参照)
BindingAdapter
@BindingMethodは既存のメソッドへのリンクでしたが、@BindingAdapterは実装を定義できます。
以下のTextViewを書いたとします。
<layout>
<data>
<variable
name="person"
type="foo.bar.Person"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{person}"/>
</layout>
TextViewのandroid:textに、Person型の変数を代入しています。
通常、TextViewにはPerson型を受け取るsetText()メソッドは存在していないため、これはビルドエラーとなります。
しかし、ここで@BindingAdapterを利用することで、この属性定義を有効にすることができます。
@BindingAdapterは任意のクラスに定義できます。(競合する場合は、先に見つかったほうが優先されるっぽいです)
public class CustomTextViewBindingAdapter {
@BindingAdapter("android:text")
public static void setText(TextView textView, Person person) {
String name = person != null ? person.getName() : null;
textView.setText(name)
}
}
@BindingAdapterの定義ルールは、
- public static なメソッドであること
- 第一引数を適用対象のViewとすること
- サブクラスにも適用される
- 第二引数以降とアノテーションの
valueの属性の順番を合わせること
です。
上の定義では、第一引数がTextViewなので、TextViewとそのサブクラスに適用されます。
第二引数はPerson型なので、android:textにPerson型の場合にこのメソッドが呼び出されます。
また、valueには複数の属性を書けるので、
@BindingAdapter({
"firstName",
"lastName"
}, requireAll = false)
public static void setText(TextView textView, String firstName, String lastName) {
String name = String.format(%s%s,
lastName != null ? lastName : "",
firstName != null ? firstName : "");
textView.setText(name);
}
こんな書き方もできます。
requireAllがtrueの場合は、指定した属性定義されている場合だけ呼ばれます(デフォルトtrue)
android:以外を書くか、または何も書かない場合はapp:namespaceに割り当てられます。
デフォルトのTextViewBindingAdapterでは、addTextChangedListener(TextWatcher)の各コールバックを分割してくれているので、android:onTextChangedなんて属性が使えたりします。
Tips
イベントハンドリング
公式のサンプルだと、メソッドリファレンス使えるアピールするためかHanlderに定義するみたいなめんどくさいことしてますが、普通にやるんでも大丈夫です。
<layout>
<data>
<variable
name="onTextChanged"
type="android.databinding.adapters.TextViewBindingAdapter.OnTextChanged"/>
<variable
name="onOkClick"
type="android.view.View.OnClickListener"/>
</data>
<LinearLayout>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onTextChanged="@{onTextChanged}"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
android:onClick="@{onOkClick}"/>
</LinearLayout>
</layout>
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setOnTextChanged((s, start, before, count) -> {
// TextWatcher.onTextChanged()
});
binding.setOnOkClick((v) -> {
// click ok button
});
}
}
Viewにid振って自分でsetOnClickListener()するんでも問題ないです。
RecyclerViewにセパレータ入れるやつ
参考
Data Binding Library | Android Developers
[bindingAdapters | Android]
(https://android.googlesource.com/platform/frameworks/data-binding/+/android-7.0.0_r29/extensions/baseAdapters/src/main/java/android/databinding/adapters)