[Android] - Data Bindingつかってみた
釘宮愼之介
こんにちは。Androidエンジニアの釘宮です。
Google I/O 2015での新しい発表の一つにData Bindingがありましたね。
Data BindingとはXMLなどのデータソースUIを静的または動的に結合する技術のことです。今まではMicrosoftのWPFなどで使われていた技術です。
今回はそのData Bindingについて、導入方法から簡単な使い方、ちょっとだけ踏み込んだ使い方、そしてこれを用いてMVVMを実現するならどういう風に組めばいいのかについて説明したいと思います。
それぞれのケースのサンプルコードも用意しております。参考になれば幸いです。
導入方法
Android Stuiod 1.3 previewである必要があります。
rootにあるbuild.gradleに下記を付け加えます。
dependencies {
classpath "com.android.tools.build:gradle:1.3.0-beta1"
classpath "com.android.databinding:dataBinder:1.0-rc0"
}
次に対象moduleのbuild.gradleに下記を付け加えます。
apply plugin: 'com.android.databinding'
以上で準備は完了です。
簡単な使い方
Inmutableなデータオブジェクトをbindする
まずはInmutableなオブジェクトを対象に早速bindしてみます。
例えば、下記のようなUserクラスがあったとしてこれらの情報を表示させてみたいと思います。
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
firstNameとlastNameを表示するlayout(activity_main.xml)は下記のように書くことができます。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="kgmyshin.databindingsample.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity"
>
<TextView
android:id="@+id/first_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
/>
<TextView
android:id="@+id/last_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
/>
</LinearLayout>
</layout>
layout
タグで全体を囲んで、data
タグ内でimportや宣言を書きます。
今回はimport文はなく宣言だけをしています。variable
タグで使うクラスと変数名を宣言します。
import文を使用する場合は下記のような書き方になります。
<data>
<import type="kgmyshin.databindingsample.User"/>
<variable
name="user"
type="User" />
</data>
このvariableでの宣言によって各コンポーネント内の値にuser
オブジェクトを使用できます。
使用するには、上記の例のandroid:text="@{user.lastName}"
のように@{}
で挟みます。
Activityで実際にbindするコードは下記です。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("shinnosuke", "kugimiya");
binding.setUser(user);
}
}
ActivityMainBinding
のオブジェクトを作って、setUser
してあげます。
これを実行すると、自分でsetTextなどしていなくても下記のようにfirstName, lastNameが表示されていることを確認できます。
補足
○○Bindingクラス
ActivityMainBindingというクラス名はlayoutのファイル名に依存しています。
例えば、view_item.xml
というファイルの場合はViewItemBinding
クラスができます。
set○○メソッド
例ではオブジェクトをbindするための関数として binding.setUser というメソッドがありますが、この関数名は下記のnameに依存しています。
<variable
name="user"
type="kgmyshin.databindingsample.User" />
例えば、name=adminUser
とした場合は、setAdminUser
という関数名になります。
データクラスでpublicフィールドを使いたくない場合
例ではpublicフィールドでやってますが、Userクラスを下記のように書き換えてしまっても問題ありません。
public class User {
private final String mFirstName;
private final String mLastName;
public User(String firstName, String lastName) {
this.mFirstName = firstName;
this.mLastName = lastName;
}
public String getFirstName() {
return mFirstName;
}
public String getLastName() {
return mLastName;
}
}
publicなメンバがいるか、もしくはgetterがあれば動きます。
メンバの先頭にmが付いていたとしてもlayoutにはuser.firstName
と書く必要があります。
mutableなデータオブジェクトをbindする
例えば押すとUserオブジェクトのlastNameが"マイケル"になってしまうボタンがあったとしましょう。
bindと謳ってるからには、表示されているそちらにも反映されてほしいものですが、先のinmutableな例では表示後に、UserオブジェクトのlastNameをマイケルに変えても表示上はマイケルになりません。
Userオブジェクトの変更をViewにも反映するには下記のようにUserクラスを書き換えます。
public class User extends BaseObservable {
private String firstName;
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Bindable
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
@Bindable
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
@Bindableをgetterにつけ、値が変わるときにnotifyPropertyChangedを呼ぶようにしました。これで変更をViewに反映することができるようになります。
実際にボタンを配置した結果の動きが下記です。
補足
@Bindableの位置
例では@Bindableをgetterにつけてますが、下記のようにfieldにつけても動きます。
@Bindable
private String firstName;
また@Bindableをつけて初めて、BR.firstNameという定数ができます。
Viewのidがメンバー名になる
例では出してませんが、今回ボタンを配置しました。
その際にxmlに下記を追加しています。
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="マイケル"
/>
id名をbtn
とすることで、java側でbinding.btn
でアクセスすることができます。
仕組み
layoutを作った時、また@Bindableを付与した時にActivityMainBindingの中でexecuteBindings()とonChangeXXX()というメソッドが自動生成/更新されます。
例えば、先の例ではこれが生成されます。
private boolean onChangeUser(kgmyshin.databindingsample.User user, int fieldId) {
switch (fieldId) {
case BR.firstName:
synchronized(this) {
mDirtyFlags |= 0b10L;
}
return true;
case BR.lastName:
synchronized(this) {
mDirtyFlags |= 0b100L;
}
return true;
case BR._all:
synchronized(this) {
mDirtyFlags |= 0b1L;
}
return true;
}
return false;
}
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
kgmyshin.databindingsample.User user = mUser;
java.lang.String firstNameUser = null;
java.lang.String lastNameUser = null;
if ((dirtyFlags & 0b1111L) != 0) {
if ((dirtyFlags & 0b1011L) != 0) {
// read firstName~.~user~
if ( user != null) {
firstNameUser = user.getFirstName();
}
}
if ((dirtyFlags & 0b1101L) != 0) {
// read lastName~.~user~
if ( user != null) {
lastNameUser = user.getLastName();
}
}
}
// batch finished
if ((dirtyFlags & 0b1011L) != 0) {
// api target 1
this.firstName.setText(firstNameUser);
}
if ((dirtyFlags & 0b1101L) != 0) {
// api target 1
this.lastName.setText(lastNameUser);
}
}
notifyPropertyChanged(BR.YYY)が呼ばれると、onChangeXXX()が呼ばれ変更されたメンバに該当するflagが立って、その後executeBindings()が呼ばれ各UIの更新が実行されます。setXXX()メソッドではじめにbindした時もexecuteBindings()は呼ばれます。
これが一連の流れです。
踏み込んだ使い方
onClickListenerなどもbindできる
Data Bindingを使えば、例えばButtonにクリックリスナーをbindすることが可能です。
下記のようなlayoutを用意します。
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<data>
<variable
name="activity"
type="kgmyshin.databindingsample.MainActivity" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Click"
app:onClickListener="@{activity.showToastListener}"
/>
</RelativeLayout>
</layout>
そしてActivity側でshowToastListenerを提供するgetterまたはpublic fieldを用意することで、btnにリスナーが登録されます。
public class MainActivity extends AppCompatActivity {
private final View.OnClickListener showToastListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "clicked", Toast.LENGTH_SHORT).show();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setActivity(this);
}
public View.OnClickListener getShowToastListener() {
return showToastListener;
}
}
app:onClickListener="@{activity.showToastListener}"
は app:setOnClickListener="@{activity.showToastListener}"
としても問題ありません。
これはButtonクラスにsetOnClickListenerという関数があるために"app:onClickListener"というattributeを追加することができています。
他のViewでもsetXXXというものはbindすることができます。
例えばDrawerLayoutの場合はsetScrimColorというメソッドがあるので下記のように書くことができます。
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"/>
本来はないattributeなのでandroid:
ではなくてapp:
となっていることと、xmlns:app="http://schemas.android.com/apk/res-auto"
が追加されていることに気をつけましょう。
まとめると、クラスにsetterさえ用意されていればなんでも簡単にbindできるということです。
カスタム attributeを作成する
setterがないものはbindできないのかというと、そうでもありません。
attributeは自作することができます。
例えばTextViewに大文字の文字列を設定するattributeを作ってみます。
layoutファイルは下記のようになります。
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<data>
<variable
name="user"
type="kgmyshin.databindingsample.User" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:capText="@{user.name}"
/>
</RelativeLayout>
</layout>
次に下記のstaticメソッドを実装します。
@BindingAdapter("capText")
public static void setCapText(TextView view, String text) {
view.setText(text.toUpperCase());
}
BindingAdapterに先ほどの"capText"という文字列を設定して、setCapTextという関数を実装します。
そして第一引数にbindされる型のTextView、第二引数にbindする型のStringを受け取るようにします。
このメソッドの実装場所は?という問が出てくると思うんですが、答えは「どのクラスでもいい」です。1)微妙な仕様。
ちなみに複数ある場合はおそらくは一番初めに見つかったものが使われるような動きでした。
例では"DataBindingSampleBinder"という拡張したattribute用の関数だけを置くクラスを作って、そこに実装しました。
ListViewにリストをbindする
カスタムattributeを使ってListViewにリストをbindすることもできるようになります。
例えば、Taskを管理するアプリケーションを作成すると想定して、そのアプリで使われるListViewを作ってみました。
bindingに関するところだけ説明します。あらかじめ、データクラスのTask、ListViewに設定するTaskAdapterクラスを作っておきます。
ListViewにカスタムattributeを追加するため、下記のような関数を実装します。
@BindingAdapter("items")
public static void setItems(ListView listView, List<Task> tasks) {
TasksAdapter adapter = new TasksAdapter(listView.getContext());
adapter.addAll(tasks);
listView.setAdapter(adapter);
}
前回のようにlayoutに下記のように書くことでbindできるようになります。
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="kgmyshin.databindingsample.Task"/>
<import type="java.util.List"/>
<variable
name="tasks"
type="List<Task>" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TasksActivity">
<ListView
android:id="@+id/task_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:items="@{tasks}"
/>
</RelativeLayout>
</layout>
あとはjava側でbindしてあげれば完了です。
public class TasksActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityTasksBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_tasks);
binding.setTasks(TaskRepository.getInstance().findAll());
}
}
以上までを実装して起動すると、ListViewにTaskリストがbindしていることが確認できます。
type="List"について
先ほどのこの部分。
<variable
name="tasks"
type="List<Task>" />
この type="List
type="" では動かないので注意が必要です
。2)こればかりはいただけない。
Taskリストから個別のTaskの画面に遷移し、そこでTaskを編集して戻った時にリストの内容は変わるか?
変わります。
"個別ページ"と"ListViewのItemView"でTaskオブジェクトをbindすることでどちらにも変更は反映されるようです。3)ただし、ある程度の規模のあるものの場合、各画面が密にまたドメイン層とプレゼンテーション層が密になりすぎるように感じるのでよくないかも。
layout.xmlの中で何ができるのか?
layout.xml内ではthis
、new
、super
の3つが使えませんが、それ以外では四則演算、シフト演算、ビット演算、論理演算などほとんどの演算子を使うことができ、またメソッド呼び出しやキャストなどもできます。
さらにJavaにはないnull結合演算子も使えます。
user.name ?? "マイケル"
例えば上記は、user.nameがnullでなければuser.nameを、nullなら"マイケル"が使うという動きをします。
まだエラーが多い
正しく書いてかつ実行できるのに、エラーが消えないということが多々あります。そのため、正しく実装できているかどうかの判断はビルドの可否で判断することになります。
もしMVVMで実装するとしたら
もしMVVMで実装するとした場合のこのData Bindingの使い所はどうなるのか考えてみました。
例えばMicrosoftのWPFではどのようにData Binding使っているのかを図にすると、こうなります。
XAMLというのはXMLベースマークアープ言語で書かれたファイルを指します。ここでUIの外観と構造を記述します。コードビハインドにその補助的なコードを書きます。コードビハインドには初期化処理だけが書かれている状態が理想的だそうです。
ViewModelではプレゼンテーションロジックとステートをもちます。ここでモデルとのやりとりを行います。
DataBindingはView(XAMLとコードビハインド)とViewModelをつなげるために使用します。
これをAndroidで行うなら、そのままXAMLはlayout.xmlに置き換わり、View,Activity等はコードビハインドとして扱われるべきです。
つまり、layout.xmlには外観だけでなく、リスナーなどもできる限りbindして、ViewやActivityなどはできる限り最小限に組むことになります。
そしてViewModelを作ってそちらにModelとのやりとりやプレゼンテーションロジックをもつようにして、今までのようにEntityに直接bindするのはやるべきではないようです。
以上WPFのMVVMをAndroidで行うなら、このやり方でやるべきという解説でした。
ただ、前例がないのでこれで破綻しないのかどうかはわかりません。まずは個人的にこのやり方で使ってみて、よさそうだったら報告させていただこうと思います。
リスクを小さめに使ってみたいのなら、リスナーなどのbindやカスタムattributeなどは一切使わずに、android:text="@{user.name}レベルのものだけに抑えるとしたほうが良さそうです。
サンプルコード
それぞれのケースにおけるサンプルコードを組んでみたので、参考にしていただけたら幸いです。
- Inmutableなデータオブジェクトをbindする
- mutableなデータオブジェクトをbindする
- onClickListenerなどもbindできる
- カスタム attributeを作成する
- ListViewにリストをbindする
まとめ
DataBindingのメリットは、いままでObserverパターンなりEventBusなりで自力で書いていたところをxmlに記述するだけで良いという点にあると思います。それに対して、Android Studioがまだbetaであることを除いても、デメリットが見えきっていません。加えて実績もまだないので、使う人は人柱力になる覚悟が必要のようです。