読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

Android Data Binding Library 雑感

Google I/O 2015!! Data Bindingのサポート出ましたね。とりあえずアレコレ触ってみた雑感を書きます。Data Bindingの使い方や機能全般に関する説明はData Binding Guide | Android Developersを参照してください。

Android Data Binding LibraryはまだPreview版です振る舞いやシンタックスが今後変わる可能性があるので本エントリの情報と差が出てくる可能性があります。ご注意ください。

Android SDK Platform-Toolsを更新しないと動かないぽいので注意

Data Binding Guide | Android Developers通りにbuild.gradleを設定するだけではダメだった。Android SDK Platform-Toolsも更新しないと〜

f:id:sys1yagi:20150529214406p:plain

Kotlinワンチャンあるで

Keynoteとかではーミリも登場しなかったKotlin。時代はC++かと思われましたが、Data Bindingを使うためにbuild.gradleにapply plugin: 'com.android.databinding'と書くと以下の様に色々dependenciesが追加されます。そこには元気に走り回るKotlinの姿が!

apply pluginするとdependenciesのprovidedにcom.android.databinding:compiler:1.0-rc0が追加され、そいつがKotlinに依存している様です。com.android.databinding:compilerはソース生成用のdependenciesなのでアプリ側へは影響を与えません。

provided - Classpath for only compiling the main sources.
\--- com.android.databinding:compiler:1.0-rc0
     +--- com.google.guava:guava:17.0
     +--- org.apache.commons:commons-lang3:3.3.2
     +--- commons-codec:commons-codec:1.10
     +--- com.android.databinding:baseLibrary:1.0-rc0
     +--- org.jetbrains.kotlin:kotlin-stdlib:0.11.91
     |    \--- org.jetbrains.kotlin:kotlin-runtime:0.11.91
     +--- com.tunnelvisionlabs:antlr4:4.4
     |    +--- com.tunnelvisionlabs:antlr4-runtime:4.4
     |    |    +--- org.abego.treelayout:org.abego.treelayout.core:1.0.1
     |    |    \--- com.tunnelvisionlabs:antlr4-annotations:4.4
     |    +--- com.tunnelvisionlabs:antlr4-annotations:4.4
     |    +--- org.antlr:antlr-runtime:3.5.2
     |    \--- org.antlr:ST4:4.0.8
     |         \--- org.antlr:antlr-runtime:3.5.2
     \--- commons-io:commons-io:2.4

公式ライブラリに使われるだなんてKotlinワンチャンあるで!

ソースが生成されて良い

で、com.android.databinding:compilerコンパイル時にBindingが定義されたLayout XMLからBinding用のソースを吐き出します。という事は実行時のSDK Versionはそんなに気にしなくてよさそうです。以下の様な単純なBindをするXMLを書くと、

activity_simple.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

  <data>
    <variable name="user" type="com.sys1yagi.databindingsample.models.User"/>
  </data>

  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical">

    <TextView
        android:id="@+id/text"
        android:text="@{user.name}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/description"
        android:text='@{"description:"+user.description}'
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
  </LinearLayout>
</layout>

こんなクラスが吐かれます。

ActivitySimpleBinding.java

package com.sys1yagi.databindingsample.databinding;
import com.sys1yagi.databindingsample.R;
import com.sys1yagi.databindingsample.BR;
import android.view.View;
public class ActivitySimpleBinding extends android.databinding.ViewDataBinding {
  //略
  private final android.widget.LinearLayout mboundView0;
  private final android.widget.TextView mboundView1;
  private final android.widget.TextView mboundView2;
  // variables
  private com.sys1yagi.databindingsample.models.User mUser;
  
  public ActivitySimpleBinding(View root) {
    super(root, 1);
    final Object[] bindings = mapBindings(root, 3, sIncludes, sViewsWithIds);
    setRootTag(root);
    this.mboundView0 = (android.widget.LinearLayout) bindings[0];
    this.mboundView0.setTag(null);
    this.mboundView1 = (android.widget.TextView) bindings[1];
    this.mboundView1.setTag(null);
    this.mboundView2 = (android.widget.TextView) bindings[2];
    this.mboundView2.setTag(null);
    invalidateAll();
  }
  //略
  public void setUser(com.sys1yagi.databindingsample.models.User user) {
    updateRegistration(0, user);
    this.mUser = user;
    synchronized(this) {
      mDirtyFlags |= 0b1L;
    }
    super.requestRebind();
  }
  public com.sys1yagi.databindingsample.models.User getUser() {
    return mUser;
  }
  //略
}

ソースが見えるのはいいですね。軽く読むとbind対象のViewとModelをfieldに持ち、setUser(User)が生えていて、いい感じにbindしてくれそうな気配を感じます。

Layout XMLにidを振るとそのViewがBindingクラスに生えるので良い

Layout XMLにidを振ると...

activity_views_with_ids.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical">

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/ok_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
  </LinearLayout>
</layout>

こんな感じで生える。

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  ActivityViewsWithIdsBinding binding = DataBindingUtil
      .setContentView(this, R.layout.activity_views_with_ids);
  binding.name.setText("jiro");
  binding.description.setText("I'm the second son.");
  binding.okButton.setText("ok");
}

もうfindViewByIdは要りません。これだけでも価値がある気がしますね。

ModelでBaseObservableを継承すれば、auto updateができる

通常のBindingはXXXBinding.setHoge()しないと反映されないですが、Model側を更新したらbindしたViewに即時反映させるといった動作も実現できます。

こんな感じ。

public class User extends BaseObservable {
  long id;
  public String name;
  public String description;

  public long getId() {
    return id;
  }
  public void setId(long id) {
    this.id = id;
  }
  @Bindable
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
    notifyPropertyChanged(BR.name);
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
}

ModelでBaseObservableを継承し*1、auto updateしたいgetterに@Bindableを付けて、setterでnotifyPropertyChanged(BR.name);という風に呼べばOK。BR.nameR.javaのBinding版みたいなもの。Layout XML定義したvariableや、@Bindableの対象がBRに生えます。

あとは使うだけ!

ActivityAutoUpdateBinding binding = 
  DataBindingUtil.setContentView(this, R.layout.activity_auto_update);
User user = new User();
user.setName("Saburo");
user.setDescription("hehehe");
binding.setUser(user);

binding.textInput.setText(user.getName());
binding.textInput.addTextChangedListener(new TextWatcher() {
  @Override
  public void afterTextChanged(Editable s) {
    //ここでbindしたViewの方にも変更が反映される
    user.setName(s.toString());
  }
  //略
});

Modelを変更、というかnotifyPropertyChanged()を実行するsetterを呼び出すとbindしているView側にも変更が反映されます。楽ちんやで。

コンパイルエラーは結構謎いので気をつける必要がある

Layout XML側でbindするfield名が間違っていてもその場では教えてもらえません。

f:id:sys1yagi:20150529215100p:plain

コンパイルすると以下のようなエラーが。

f:id:sys1yagi:20150529215109p:plain

うーん情報に乏しい。ActivitySimpleBindingのエラーなどが含まれているという事からData Bindingのエラーだろうという事がわかります。しかしどこが死んでるのかはわかりません。全体的に死ぬっぽいので数が増えると特定が難しくなるかもしれませんね。

なんでも大体bindingもできる

コンパイル時にコード生成するので、結構力技っぽい事ができるっぽいです。android:onItemClickListener="@{listener}"という風に本来のattributeに存在しないものも書けます。

<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
    <variable name="listener" type="android.widget.AdapterView.OnItemClickListener"/>
  </data>
  <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:onItemClickListener="@{listener}"
        />
</layout>

で、こう

binding.setListener(new AdapterView.OnItemClickListener() {
  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    //do something             
  }
});

なんかこう、色々publicなsetterがあるやつは大体bind対象にできる気配があります。なんかこうこれに関しては微妙な気配を感じます。何が嬉しいんだろう、Bindingにレイアウトの持つプロパティを書けるぜみたいな感じなんだろうか。

サンプルコード

コチラにまとめてます。

https://github.com/sys1yagi/data-binding-sample

まとめ

まだ触ってない機能一杯ありますが、とりあえずサイコーでは。正式版はよ

*1:BaseObservableは実装を容易にする為に用意されたものです。継承が厳しい場合はObservable interfaceを実装すればOK