コードでできること

ひととおりLayout XML上でできることを解説しました。本節ではBindingクラスの操作やObservableインタフェースを使ったデータの自動反映などコード側でできることを解説します。

Bindingクラスの作成方法

Hello Worldの節ではDataBindingUtilクラスのsetContentView()メソッドを使ってBindingクラスの作成を行いました。DataBindingUtilクラスのsetContentView()メソッドはActivityクラスのsetContentView()メソッドをラップしたものなので他の用途では使えません。本項ではBindingクラス自身が持つbind()とinflate()という2つのstaticメソッドの機能と利用方法について解説します。

bind()メソッドは指定したViewをもとにBindingクラスを作成するメソッドです。リスト23のようにFragmentのonCreatedView()などのViewを受け取るような部分で利用することが考えられます。

リスト2.23 bind()を使ってBindingクラスを作成する

public class MainFragment extends Fragment {

  FragmentMainBinding binding;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
          Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_main, container, false);
  }

  @Override
  public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    binding = FragmentMainBinding.bind(view);
  }
}

inflate()メソッドはLayoutInflaterを渡すことでレイアウトをinflateしつつBindingクラスも作成するメソッドです。リスト24のようにArrayAdapterのgetView()メソッドで利用したり、ListViewのHeader Viewを作るために利用したりすることが考えられます。

リスト2.24 inflate()を使ってBindingクラスを作成する

public class ArticleAdapter extends ArrayAdapter<Article> {

  public ArticleAdapter(Context context) {
    super(context, -1);
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
      ArticleBinding binding =
        ArticleBinding.inflate(LayoutInflater.from(getContext()));
      convertView = binding.getRoot();
      convertView.setTag(binding);
    }
    ((ArticleBinding) convertView.getTag()).setArticle(getItem(position));
    return convertView;
  }
}

Observableによる値の自動反映

バインドする値が不変(Immutable)な場合、Bindingクラスに一度データをセットすればその後変化することはないので問題はでませんが、可変(Mutable)な場合データを更新したときに再度Bindingクラスにデータをセットする必要があります。リスト25はデータの更新が発生したときに再度Bindingクラスにデータをセットする例です。

リスト2.25 データが更新されたらもう一度バインドする

api.updateTitle(text, new Callback(){
  @Override
  public void onSuccess(String updated){
    article.setTitle(updated);

    // タイトルが更新されたのでもう一度bindingに渡す
    // 値を操作する側で呼び出さないといけないので面倒くさい
    binding.setArticle(article);
  }
});

少し面倒ですね。android.databinding.Observableインタフェースと@Bindableアノテーションを使うと更新の監視と反映を自動的に行えます。ArticleクラスでObservableインタフェースを実装してみましょう(リスト26)。

リスト2.26 ArticleクラスでObservableを実装する

public class Article implements Observable {
  // コンストラクタや他のプロパティは省略しています

  PropertyChangeRegistry registry;

  String title;

  @Bindable
  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
    registry.notifyChange(this, BR.title);
  }

  // Observableインタフェースのメソッド
  @Override
  public void addOnPropertyChangedCallback(OnPropertyChangedCallback cb) {
    registry.add(cb);
  }

  @Override
  public void removeOnPropertyChangedCallback(OnPropertyChangedCallback cb) {
    registry.remove(cb);
  }
}

Observableを実装するとBindingクラス側でバインド処理をするときにaddOnPropertyChangedCallback()を呼び出してコールバックを登録してくれるようになります。データが更新されたときに登録されたコールバックを呼び出せばBindingクラスのバインド処理が再度実行されます。コールバックを自前で保持してもよいですが予め用意されているPropertyChangeRegistryを利用すると管理が楽になります。ArticleクラスのgetTitle()メソッドに@Bindableアノテーションを付与しています。これによりBR.javaが生成されます。イメージとしてはR.javaと同じです。BR.javaには@Bindableアノテーションをつけたプロパティ名に対応するプロパティIDが定義されます。Bindingクラスから受け取ったコールバックに対してプロパティIDを指定すると対応する値のバインド処理が実行されるという仕組みです。これによりモデルに対して変更を行ったときに自動的にViewに反映させることができます。

api.updateTitle(text, new Callback(){
  @Override
  public void onSuccess(String updated){
    // コールバックによってバインドが再度実行される
    article.setTitle(updated);
  }
});

BaseObservableによる値の自動反映

前項ではObservableインタフェースを自前で実装しました。BaseObservableクラスを利用するとより簡単に自動更新を実装できます。リスト27のように@Bindableアノテーションの付与と、更新時の呼び出しだけを記述すればよくなります。

リスト2.27 ArticleクラスでBaseObservableを継承する

public class Article extends BaseObservable {
  // コンストラクタや他のプロパティは省略しています

  String title;

  @Bindable
  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
    notifyChange(BR.title);
  }
}

ただし、BaseObservableクラスは抽象クラスである点に留意して下さい。継承が行えないクラスで自動更新を実現したい場合はObservableを実装するか次項で説明するObservableFieldの利用を検討してください。

ObservableFieldを使った値の自動反映

ObservableFieldクラスを使うとObservableインタフェースやBaseObservableクラスを使わなくても自動更新を実現できます。リスト28はObservableFieldを利用する例です。ObservableField自身がget/setメソッドを持っているのでpublicにアクセスできるように書いています。

リスト2.28 ObservableFieldクラスを使う

public class Article {
  // コンストラクタや他のプロパティは省略しています

  public final ObservableField<String> title = new ObservableField<>();
}

直接ObservableFieldに値をセットするとバインド処理が実行されます。

api.updateTitle(text, new Callback(){
  @Override
  public void onSuccess(String updated){
    article.title.set(updated);
  }
});

もちろんgetter、setterを用意することもできます(リスト29)。

リスト2.29 getter、setterを用意する

public class Article {
  // コンストラクタや他のプロパティは省略しています

  final ObservableField<String> title = new ObservableField<>();

  public ObservableField<String> getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title.set(title);
  }
}

ここで注目したいのは、getter、setterで取り扱う型が違っている点です。getterではObservableFieldクラスを返していますがsetterではStringを受けるスタイルで書いています。どちらかというとgetterもStringを返す形にしたい所です。しかしgetTitle()メソッドでStringを返すと自動反映の機構が失われます。これはBindingクラス生成時にプロパティの型をStringと判定してしまう為です。次のように別名でgetterを作成すれば回避できます。

public String getTitleValue() {
  return title.get();
}

Data Binding Libraryの特殊な機能

本節ではData Binding Libraryが用意している少し特殊な機能に触れていきます。Data Binding Libraryの導入初期にはあまり関係ないものや、慎重な設計が必要なものなどが含まれますのでご注意ください。

Automatic Setterで存在しない属性値へバインドする

バインド処理は生成されたBindingクラスで行われます。つまりコード上で行われるわけです。例えばandroid:text属性にバインドする場合setText()メソッドが利用されます。他の属性についても同様です。この性質を使ってandroid名前空間には存在しない属性でも対象のView持つsetterメソッドに対応する値をバインドの対象にできます。この仕組みをAutomatic Setterと呼びます。リスト30はListViewのonItemClickListenerをバインドする例です。onItemClickListenerはandroid名前空間には存在しないのでカスタム属性を利用します。

リスト2.30 OnItemClickListenerをバインドする

<layout
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:bind="http://schemas.android.com/apk/res-auto">
  <data>
    <variable name="onItemClick"
     type="android.widget.AdapterView.OnItemClickListener"/>
  </data>
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical">
    <ListView
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      bind:onItemClickListener="@{onItemClick}"/>
  </LinearLayout>
</layout>

リスト31のようにBindingクラス越しにセットできるようになります。

リスト2.31 OnItemClickListenerをBindingクラスにセットする

ActivityListviewBinding binding = DataBindingUtil
                .setContentView(this, R.layout.activity_listview);
binding.setOnItemClick(new AdapterView.OnItemClickListener() {
  @Override
  public void onItemClick(AdapterView<?> parent, View view,
    int position, long id) {
      // do something
    }
  });

Automatic Setterは次の規則で変換したメソッドが対象のViewに存在すれば利用できます。

  • 属性名の先頭に"set"を付与する
  • キャメルケースに変換する
  • 引数が1つである

対応するsetterを持たない属性値を@BindingAdapterを使ってバインドする

Automatic Setterは対象のViewが持つsetterメソッドを属性値のように取り扱うことができる仕組みです。Automatic Setterではandroid:paddingLeft属性のように対応するsetterメソッドを持たない属性をカバーできません。@BindingAdapterアノテーションを使うと特定のメソッドをバインド処理の際に呼び出せるようになります。これによってsetterメソッドを持たない属性についてもバインドができるようになります。リスト32はandroid:paddingLeftをバインドする際に実行するメソッドを定義した例です。setPadding()メソッドでpaddingLeftの部分だけ受け取った値をセットしています。paddingRightやpaddingUpなどについても同様の方法でカバーできます。

リスト2.32 @BindingAdapterの利用例

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
 view.setPadding(padding,
         view.getPaddingTop(),
         view.getPaddingRight(),
         view.getPaddingBottom());
}

@BindingAdapterアノテーションを付与するには次の条件を満たさなければなりません。

  • staticメソッドであること
  • 第一引数に対象となるViewを取ること
  • 第二引数以降に属性値を取ること

メソッドを定義する場所については規定がありません。また同じ属性について複数のメソッドを定義することは出来ないので注意しなければなりません。つまりBindingクラス毎に利用するメソッドを変えるといったことはできません。この性質から@BindingAdapterアノテーションを利用する際は慎重になる必要があります。専用のパッケージやクラスを作り、@BindingAdapterアノテーションを付与したメソッドが一箇所に集まるように設計するべきでしょう。

@BindingAdapterを使って複雑な処理を属性値で記述できるようにする

@BindingAdapterで指定できる属性値はandroid名前空間に限りません。独自に属性を作ってバインド時の処理に呼び出させるといったことができます。URLで指定した画像の表示方法を指定できるカスタム属性を追加する例を考えてみます。ImageLoadAdapterクラスを作成し、画像のURLを受け取って表示するだけのメソッドを定義します(リスト33)picasso

リスト2.33 ImageLoadAdapter.java

public class ImageLoadAdapter {
  @BindingAdapter({"bind:imageUrl"})
  public static void bindImage(ImageView view, String url) {
    Picasso.with(view.getContext()).load(url).into(view);
  }
}

これによりLayout XMLで次のような記述が可能となります。

<ImageView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  bind:imageUrl='@{user.icon}'
  />

"bind:imageUrl"というふうに名前空間を一致させていますがそういった制約はありません。@BindingAdapterアノテーションでの属性名の定義では名前空間を省略できますが、android名前空間かそうでないのかを判別できるようにするために名前空間は省略せずに書くことをお勧めします。次はimageUrl属性に加えてcorner属性を持っていた場合に実行するメソッドをImageLoadAdapterに追加します(リスト34)。corner属性は角丸の半径を表します。このメソッドでは受け取ったcornerを使って画像を加工しています。

リスト2.34 属性値を組み合わせる

@BindingAdapter({"bind:imageUrl", "bind:corner"})
public static void bindImage(ImageView view, String url, int corner) {
  Context context = view.getContext();
  Picasso.with(context).load(url)
          .transform(new RoundedCornerTransformation(corner))
          .into(view);
}

リスト35は追加した2つのメソッドを属性値の組み合わせで使い分ける例です。

リスト2.35 属性値を組み合わせて使う

<TextView
  android:text="normal"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>
<ImageView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  bind:imageUrl='@{user.icon}'
  />
<TextView
  android:text="corner"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>
<ImageView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  bind:imageUrl='@{user.icon}'
  bind:corner="@{8}"
  />

実行すると図2のように表示されるはずです。Layout XML上で表示の形式を柔軟に設定できるようになりました。

それぞれ@BindingAdapterで定義したメソッドが実行される

@BindingAdapterアノテーションをどのように利用するべきかはまだまだ議論の余地があります。すぐにどこの何のメソッドが実行されるかわからなくなってしまうでしょう。どのように運用するのか予め決めておく必要があるでしょう(リスト36)。

リスト2.36 どのように利用すべきか慎重に検討が必要

// 組み合わせではなく明示的に名前を分ける方がよいかもしれない
@BindingAdapter({"bind:imageUrl"})
@BindingAdapter({"bind:imageUrlWithCorner", "bind:corner"})

// どこで定義されているか明示するべきかもしれない
// でもこれは大げさに感じる
@BindingAdapter({"bind:ImageLoadAdapter_imageUrl"})
picasso. 画像の読み込み処理にsquare/picassoを利用しています。 https://github.com/square/picasso

定義済みの@BindingAdapterや@BindingMethod、@BindingConversion

Data Binding Libraryのプラグインを適用すると自動的に"com.android.databinding:dataBinder:adapters"がcompileのdependenciesに追加されます。このライブラリのandroid.databinding.adaptersパッケージを覗くと、@BindingAdapterアノテーションを付与したメソッドが大量に見つかります。予めsetterメソッドを持たない属性に対する処理を定義してくれているわけです。

@BindingAdapterアノテーションを付与したメソッドを持つクラス群

定義されているメソッドはすべてandroid名前空間に対するものなのでカスタム属性を使う場合は関係ありませんが、既存の属性に対する@BindingAdapterアノテーションを定義する場合は予めチェックしておくとよいでしょう。

またこれらのクラス群の中では@BindingMethodや@BindingConversionといったアノテーションが使われています。@BindingMethodアノテーションはRenamed Setterを実現するためのアノテーションです。Renamed Setterは指定した型の属性をどのsetterメソッドで処理するかを定義できます。属性名とsetterメソッドの名前が一致しない場合に使います。属性名とメソッドの対応を定義するものなのでクラスのアノテーションに記述します。

@BindingMethods({
  @BindingMethod(type = android.widget.FrameLayout.class,
    attribute = "android:foregroundTint",
    method = "setForegroundTintList"),
})
public class FrameLayoutBindingAdapter {
}

@BindingConversionアノテーションは式の戻り値の型を対象の属性値の型に変換するメソッドを定義できます。例えばandroid:background属性はDrawableを要求します。式で@colorを使った場合戻り値の型はintとなってしまうためandroid:background属性の式では利用できません。そこで@BindingConversionアノテーションを使って変換を行うメソッドを定義します。

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
  return new ColorDrawable(color);
}

これらのアノテーションも@BindingAdapterアノテーションと同様に自由に利用できます。より深いカスタマイズをしたい場合は"com.android.databinding:dataBinder:adapters"の実装を参考にするとよいでしょう。