[Android]DataBindingで双方向の通知を実装してみる

2016.05.31

Databindingはサポートライブラリで提供されているバインディングライブラリです XML上でviewとデータを関連づけたり、データ変更に伴うUI動作などのプロセスを自動化させることができ、単純にfindviewIdやButterKnifeの一部の機能の替わりとしても利用が可能です 他にも様々な便利機能が用意されてますが、導入からモデル-view間の通知までのサンプルを紹介してみます

導入

DataBindingの使用にはgradleプラグイン1.5以上が必要になります

gladle

android {
....
dataBinding {
enabled = true
}
}

もしくは

project/build.gradle

dependencies {
....
classpath "com.android.databinding:dataBinder:1.0-rc4"
}
app/build.gradle

apply plugin: 'com.android.databinding'

基本実装

モデルクラスを準備

public class Login {

private String email;
private String password;

public Login(String email, String password) {
this.email = email;
this.password = password;
}

/* getter and setter */
}

XMLに記述

<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>
        <variable name="login" type="jp.sample.model.Login"/>
    </data>

    <LinearLayout

        <EditText
            android:id="@+id/email"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textEmailAddress"
            android:singleLine="true"
            android:text="@{login.email}"/>
         ...

    </LinearLayout>

</layout>
  • ルートを< layout >にする
  • < data >は< layout >内での変数のラッパーとしての役割
  • @{ }構文でプロパティが設定される
  • クラスがnullの場合、パラメータにnullや0が設定される

データをバインド

MainActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setLogin(new Login("email","password"));
}
  • レイアウトファイル名に基づいてBindingクラスが生成される
  • set***メソッドの名前は< variable >のnameフィールドに依存する

ここまでがDataBindingを利用する上での基本的な部分になります

モデルの変更をviewに反映させる

ObservableFieldを使う

private static class Login {

public final ObservableField email =
new ObservableField<>();

public final ObservableField password =
new ObservableField<>();

}
Login login = new Login()
login.email.set("hoge")
    <EditText
         android:id="@+id/email"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:inputType="textEmailAddress"
         android:singleLine="true"
         android:Text="@{login.email}"/>   /* ObservableField */
         ...
  • モデルの値が変わるとUI(text等)も変わる
  • フィールドの型がObservableFieldになってしまう
  • プリミティブ型用にはObservableBooleanとかが用意されてる

BaseObservableを継承してつかう

public class Login extends BaseObservable {

private String email;

private String password;

public Login(String email, String password) {
this.email = email;
this.password = password;
}

@Bindable
public String getEmail() {
return email;
}

@Bindable
public String getPassword() {
return password;
}

public void setEmail(String email) {
this.email = email;
notifyPropertyChanged(BR.email);
}

public void setPassword(String password) {
this.password = password;
notifyPropertyChanged(BR.password);
}

}
Login login = new Login()
login.setEmail("hoge")
    <EditText
         android:id="@+id/email"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:inputType="textEmailAddress"
         android:singleLine="true"
         android:Text="@{login.email}"/>
         ...
  • 通知したいパラメータに@Bindableをつける
  • @BindableがついてるプロパティがBRクラスのフィールドに追加される
  • notifyPropertyChanged(int fieldId)でviewへの通知を行う
  • extendsを消費してしまう(したくない場合はObservableを実装する)

以上、上記のObservableFieldを利用したパターンとBaseObsaervableを継承したパターンがモデルの変更をviewに通知する例になります

モデルとViewの双方向で通知する

public class LoginViewModel extends BaseObservable {

private String email;

private String password;

private boolean isEditMode = false;

public LoginViewModel(String email, String password) {
this.email = email;
this.password = password;
}

public SimpleTextWatcher emailWatcher = new SimpleTextWatcher() {
@Override
public void onTextChanged(String value) {
isEditMode = true;
setEmail(value);
isEditMode = false;
}
};

public SimpleTextWatcher passwordWatcher = new SimpleTextWatcher() {
@Override
public void onTextChanged(String value) {
isEditMode = true;
setPassword(value);
isEditMode = false;
}
};

@Bindable
public String getEmail() {
return email;
}

@Bindable
public String getPassword() {
return password;
}

public void setEmail(String email) {
this.email = email;
if (!isEditMode) {
notifyPropertyChanged(BR.email);
}
}

public void setPassword(String password) {
this.password = password;
if (!isEditMode) {
notifyPropertyChanged(BR.password);
}
}

}
  • 双方向で通知がループしないようにフラグを設定
  • サンプルではTextWatcherのカスタムクラスを作成してます
public abstract class SimpleTextWatcher implements TextWatcher {

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {

}

@Override
public void afterTextChanged(Editable s) {
onTextChanged(s.toString());
}

public abstract void onTextChanged(String value);

}

カスタムしたTextWatcherをEditTextにバインドするためにaddTextChangeListenerを設定する

    <EditText
         android:id="@+id/email"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:inputType="textEmailAddress"
         android:singleLine="true"
         android:text="@{viewModel.email}"
         app:addTextChangedListener="@{viewModel.emailWatcher}"/>
         ...

これで画面上でEditTextに入力した値をモデルに適用させる事ができました

  • DataBindingの機能によって、標準でviewコンポーネントごとにいくつかのリスナー等がxmlで設定できるように用意されます
  • 上記はBaseObservableを継承した例ですが、ObservableFieldを利用したパターンでもviewからの通知は可能です。その場合はObservableのプロパティが変更された時のコールバックメソッドがあるので、それを使うと良いでしょう

まとめ

今まではモデルの変更ロジックに応じて、UIの動作を記述していましたがDataBindingの利用でActivityやFragmentがよりスッキリさせる事できそうですね