はじめに
基本的なことは省略してます。
設定
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)