[Google I/O 2016] より使いやすくなったDataBindingの新機能とTips紹介

こんにちは。Androidエンジニアの@kgmyshinです。

前回の Google I/O 2016 基調講演レポートに続き、今回はAdvanced Data Bindingというセッションをレポートします。

2015年のGoogle IOで発表されたAndroid Data Binding。

Data Bindingとは、layout.xmlにBindするオブジェクトを定義し、そのままxml内でそれぞれの各Viewに値をセットするという機構です。MicrosoftのWPFでは以前からあるお馴染みの仕組みであり、MVVMと戯れているとよく耳にするキーワードではないでしょうか。当ブログにも使い方から仕組みまで説明した記事がありますので、まだご存知でない方は是非とも一度ご覧になってください。

双方向DataBinding

少し前ですと、双方向Bindingを実現するには下記のようなコードを書く必要がありました。

<EditText android:text="@{user.name}"
          android:afterTextChanged="@{callback.change}" ../>
public void change(Editable s) {
  final String text = s.toString();
  if (!text.equals(name.get())) {
    name.set(text);
  }
}

しかし、2016年5月現在では下記のようにたった1行のコードで実現出来てしまいます。


内部でBindingが自動でInverseBindingListenerを実装することによってこれを実現しています。

双方向Bindingによる通知の無限ループの解決

『View側の値が変わるとモデル側の値も変わり、反対にモデルの値が変わるとViewの値が変わる』というのが双方向Bindingです。

これをそのまま行うと、例えば

Viewの値が変更
( 1 ) Viewの値が変更されたのを検知してモデルの値の変更
( 2 ) モデルの値の変更を検知してViewの値を変更
( 3 ) Viewの値が変更されたのを検知してモデルの値の変更
( 4 ) モデルの値の変更を検知してViewの値を変更
以下、ループ……

といったような無限のサイクルが出来上がってしまいます。残念ながら今のところこれに対するスマートな解決策はなく、下記のように以前と同じ値であれば何もしないといった記述をしてください。

@BindingAdapter("androd:text")
public static void setText(TextView view, CharaSequence text) {
  final CharaSequence oldText = view.getText();
  if (!haveContentsChanged(text, oldText)) {
    return: // no content changes.
  }
  view.setText(text);
}

Viewの値を取得できるようになった

全てではありませんが、text, checked, rating, progress 等の値を取得できるようになっています。これにより、例えば今までは下記のように辛かった繰り返しの記述もvisibilityを使う事でスマートに書く事ができます。

<ImageView android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}" />
<TextView android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}" />
<CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}" />
<ImageView
  android:id="@+id/avatar"
  android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}" />
<TextView android:visibility="@{avatar.visibility}" />
<CheckBox android:visibility="@{avatar.visibility}" />

また、下記のようにCheckBoxのcheckedを取得して表示・非表示を切り替えるといった事も出来るようになりました。

<CheckBox android:id="@+id/seeAds">
<ImageView android:visibility="@{seeAds.checked ? View.VISIBLE : View.GONE}" />

ラムダ表記とMethod Reference

例えば binding したボタンの動作を実装する場合、今までは下記のように記述していました。

<data>
  <variable name="item" type="iotalks.Item" />
</data>
<RelativeLayout>
  <Button
      android:id="@+id/save_button"
      android:text="@string/save" />
</RelativeLayout>
itemBinding.saveButton.setOnClickListener(() -> {
  controller.saveItem(item);
}

Method Reference

Method Referenceを使用する事で下記のように記述する事が出来ます。

<data>
  <variable name="presenter" type="Presenter" />
  <variable name="item" type="Item" />
</data>
<RelativeLayout>
  <Button
    android:onClick="@{presenter::OnClick}"
    android:text="@string/save" />
</RelativeLayout>
// Presenter.java
void onClick(View v)  {
  ItemBinding binding = DataBindingUtil.findBinding(v);
  Item item = binding.getItem();
  controller.saveItem(item);
}

ラムダ表記

ラムダ表記を使用すればxmlのみで事足ります。

<data>
  <variable name="presenter" type="Presenter" />
  <variable name="item" type="Item" />
</data>
<RelativeLayout>
  <Button
    android:onClick="@{() -> presenter.save(item))}"
    android:text="@string/save" />
</RelativeLayout>

ラムダ表記のルール

( 1 ) Viewを引数に追加できる

下記のようにviewを引数に追加する事ができます。


( 2 ) 引数は全部除くか全部入れるのどっちかであること

onFocusChangeにbindする際は、

View.OnFocusChangeListener() {
   public void onFocusChange(View v, boolean hasFocus) {
      :
   }
}

上記のonFocusChangeメソッドの引数と合わせるか、もしくはすべて取るかのどちらかでないといけません。

android:onFocusChange="@{()-> presenter.refresh()}" // OK
android:onFocusChange="@{(v, fcs)-> presenter.refresh(v, fcs)}" // OK
android:onFocusChange="@{(fcs)-> presenter.refresh(fcs)}" // NG

返り値の型は合わせる事

例えば下記のように記述した場合は、


showMenuOnLongClickListener.onLongClickの返り値と型(boolean)に合わせる必要があります。

// Presenter.java
public showMenu(View view) {
  :
  return true;
}

アニメーション

Transition に関しては下記のようなコードを書くだけで動きます。

binding.addOnRebindCallback(new OnRebindCallback() {
  @Override
  public boolean onPreBind(ViewDataBinding binding) {
    ViewGroup sceneRoot = (ViewGroup) binding.getRoot();
    TransitionManager.beginDelayedTransition(sceneRoot);
    return true;
  }
  :
})

その他のアニメーションに関しては値をセットするときに書くと良いです。

@BidingAdapter("adText")
public static void animateTextChanges(TextView view, String oldText, String newText) {
  if (!Objects.equals(oldText, newText)) {
    return;
  }
  animateTextChange(view, oldText, newText);
}

テストについて

テストするために下記のようなコードを書く事があるかもしれませんが、実はこれはNG。決して良い解決策とは言えません。

public class MyBindingAdapters {
  @BindingAdapter("android:text")
  public static void setText(TextView view, String value) {
    if (isTesting) {
      doTestStuff(view, value);
    } else {
      TextViewBindingAdapter.setText(view, value);
    }
  }
}

下記のように abstractBindingAdapterを定義します。

public abstract class MyBindingAdapters {
  @BindingAdapter("android:text")
  public abstract void setText(TextView view, String value);
}

そしてテストの際にはテスト用のものを作り、それをInjectすること。

class TestBindingAdapters extends MyBindingAdapters {
  @BindingAdapter("android:text")
  public abstract void setText(TextView view, String value) {
    testyTestStuff(view, value)
  }
}

そしてDataBindingComponentを実装します。

class TestComponent implements DataBindingComponent {
   private MyBindingAdapters mAdapter = new TestBindingAdapters();
   MyBindingAdapters getMyBindingAdapters() {
       return mAdapter;
   }
}

defaultにセットしてください。


DI を使用する事でテストの時のみ TestComponent を使用するということが出来るようになります。

Tips

ビジネスロジックは入れないほうがよい



ただし、UIのロジックであれば以下の書き方でもOKですが、くれぐれも長くなりすぎないように注意しましょう。


RecyclerViewではexecutePendingBindingsを呼ぶ事

void onBindViewHolder(ItemViewHodler hodler, int pos) {
   holder.mBinding.setItem(mItems.get(pos));
   holder.mBinding.executePendingBindings();
}

以上、簡単ではありますが Advanced Data Bindingのセッションレポートを現場よりお伝えしました。