[Android] - Data Bindingつかってみた

釘宮愼之介
217

こんにちは。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が表示されていることを確認できます。

inmutable

補足

○○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に反映することができるようになります。
実際にボタンを配置した結果の動きが下記です。

mutable

補足

@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できるということです。

binding_listener

カスタム 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用の関数だけを置くクラスを作って、そこに実装しました。

カスタム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していることが確認できます。

databinding-list

type="List"について

先ほどのこの部分。

<variable
            name="tasks"
            type="List<Task>" />

この type="List"の表記はミスではありません。
type="" では動かないので注意が必要です2)こればかりはいただけない。

Taskリストから個別のTaskの画面に遷移し、そこでTaskを編集して戻った時にリストの内容は変わるか?

変わります。

ListView with Databinding

"個別ページ"と"ListViewのItemView"でTaskオブジェクトをbindすることでどちらにも変更は反映されるようです。3)ただし、ある程度の規模のあるものの場合、各画面が密にまたドメイン層とプレゼンテーション層が密になりすぎるように感じるのでよくないかも。

layout.xmlの中で何ができるのか?

layout.xml内ではthisnewsuperの3つが使えませんが、それ以外では四則演算、シフト演算、ビット演算、論理演算などほとんどの演算子を使うことができ、またメソッド呼び出しやキャストなどもできます。
さらにJavaにはないnull結合演算子も使えます
user.name ?? "マイケル"
例えば上記は、user.nameがnullでなければuser.nameを、nullなら"マイケル"が使うという動きをします。

まだエラーが多い

正しく書いてかつ実行できるのに、エラーが消えないということが多々あります。そのため、正しく実装できているかどうかの判断はビルドの可否で判断することになります。

databinder-error1

databinder-error2

もしMVVMで実装するとしたら

もしMVVMで実装するとした場合のこのData Bindingの使い所はどうなるのか考えてみました。
例えばMicrosoftのWPFではどのようにData Binding使っているのかを図にすると、こうなります。

WPF-MVVM

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}レベルのものだけに抑えるとしたほうが良さそうです。

サンプルコード

それぞれのケースにおけるサンプルコードを組んでみたので、参考にしていただけたら幸いです。

まとめ

DataBindingのメリットは、いままでObserverパターンなりEventBusなりで自力で書いていたところをxmlに記述するだけで良いという点にあると思います。それに対して、Android Studioがまだbetaであることを除いても、デメリットが見えきっていません。加えて実績もまだないので、使う人は人柱力になる覚悟が必要のようです。

脚注   [ + ]

1. 微妙な仕様。
2. こればかりはいただけない。
3. ただし、ある程度の規模のあるものの場合、各画面が密にまたドメイン層とプレゼンテーション層が密になりすぎるように感じるのでよくないかも。