130
110

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DataBindingのあれこれ

Last updated at Posted at 2016-10-14

はじめに

基本的なことは省略してます。

設定

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

Android Studioおよびgradle plugin2.2.0を利用している人は、悲しいバグを踏むので2.2.1にしましょう。

制限

現状では、JackコンパイラでDataBindingは使えません
そのため、Java8の機能が使いたい場合はライブラリで補完しましょう。

機能 ライブラリ
Lambda Retrolambda
Stream API Lightweight-Stream-API

制約

  • Javaの予約語に準拠
  • contextは変数名として使えません
  • Contextインスタンスが参照できます
  • マルチバイト文字は使えません
  • マルチバイト文字のメソッドなどを呼び出しているとビルドに失敗します

DataBindingの特徴

とりあえずこのへんに触れます。

  • Null安全
  • メソッドアクセスの効率化
  • mergeタグ不要
  • DataBindingの連係
  • カスタム属性

Null安全

DataBindingでは、レイアウトに書いたコードについてはNullPointerExceptionにならないという特性があります。
特に記述されていませんが、これには配列やListのArrayIndexOutOfBoundsExceptionなども含まれています。

activity_main.xml
<layout>
    <data>
        <variable
            name="person"
            type="foo.bar.Person"/>
    </data>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{person.name}"/>

</layout>

この場合、コード側で

MainActivity.java
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setPerson(null);

と、nullをセットされていても、ぬるぽにはなりません。

android:text="@{person.name.firstName}"

この場合の、namenullの場合も同様です。
配列外アクセスの場合も同様に安全に処理してくれます。(実装上はnullであると判定される)

DataBindingのシンタックスシュガーとして、**A ?? B**というものがあります。
これは左辺Aがnullの場合に右辺Bを使うという表現ですが、これは左辺が配列外アクセスであっても同様に適用されます。

メソッドアクセスの効率化

DataBindingでは1回の画面更新で、同じ処理は1度しか行わないように実装されています。

これはどういうことかというと、

activity_main.xml
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{person.getName()}"/>

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{person.getName() + ` (` + person.getAge() + `)`}"/>

このように、2つのTextViewで同じメソッドgetName()へアクセスしている場合、生成されるコードでは以下のように使われるということです。(実際のコードとは異なります)

ActivityMainBinding.java
String name = person.getName();
int age = person.getAge();

textView1.setText(name);
textView2.setText(name + " (" + age + ")");

この特性はnullチェックにも適用され、自分で書くとめんどくさいローカル変数出しを自動で行ってくれます。

getterの特性

ちなみに、上ではperson.getName()と書きましたが、DataBindingpublicなgetterを変数アクセスのように記述できます。
person.getName()の場合person.nameとしても有効になります。(むしろ後者しか補完されません)

では、person.getName()person.nameが混在するとどうなるのか。
これは、別の処理として認識されるようで、2つの変数に分割されます。ちなみにnamepublicな変数だったとしても、person.namegetterを優先して呼び出されます

mergeタグ不要

レイアウトXMLでは、mergeタグというものが存在します。
これはincludeタグやViewGroupに対してattachToRoot=trueでレイアウトをinflateしたときに、どのような階層になるかを指定するものです。(詳細は省きます)

DataBindingはトップ階層をlayoutタグにする必要があります。それに対してmergeタグはトップ階層にしか定義できません。

ではどうするのか?

答えは、mergeタグは利用できないです。
公式を見ても、Data binding does not support include as a direct child of a merge element.としか書いてありません。

この説明、非常に不親切なんですが、実際は**mergeタグを使う必要がない**のほうが正しいです。

layoutタグが非常に柔軟にできていて、includeタグでレイアウトを挿入した場合、mergeの役割を果たしてくれます。
逆にコードでinflateした場合、必要に応じてFrameLayoutで囲ってくれます。つまり、エンジニアはこの動作について気にする必要がないということです。

DataBindingの連係

includeタグ関連でもう2つ便利な動作を紹介します。

DataBindingからDataBindingを取得する

activity_main.xml
<layout><LinearLayout><include
            android:id="@+id/foo_binding"
            layout="@layout/layout_foo"/>

    </LinearLayout>
</layout>
layout_foo.xml
<layout>
    <data>
        <variable
            name="age"
            type="int"/>  
    </data></layout>

activity_main.xmlで、includeタグにidを振ってみます。
するとコード側では、

MainActivity.java
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

Person person = new Person("山田太郎", 25);
binding.setPerson(person);

// BindingからBindingが生える
LayoutFooBinding fooBinding = binding.fooBinding;
fooBinding.setAge(person.getAge());

このように、ActivityMainBindingからLayoutFooBindingにアクセスできるようになります。

ViewDataBindingに限らず、とりあえずアクセスしたければ**idを振れば生えてくる**ということです。
逆にidを振らなければ隠蔽できます。

DataBindingからDataBindingへのデータ渡し

上の例では、コードからデータを渡していましたが、じつはこのコードはレイアウトxmlで完結させることできます。

layout_foo.xmlには、int型の変数ageが定義されています。
この場合、includeしている側のactivity_main.xmlは、includeタグの属性に以下のような記述ができます。

activity_main.xml
<layout><LinearLayout><include
            layout="@layout/layout_foo"
            app:age="@{person.age}"/>

    </LinearLayout>
</layout>

layout_foo.xmlで定義したvariableの名前をそのまま、app:<variableName>として指定できます。

カスタム属性

dataタグに定義されている変数以外に、DataBindingでは任意の属性を外から追加することができます。

定義 属性名 利用可能箇所
variableタグ app:<variableName> includeタグ
Viewpublic setter android:onClickListenerなど 対象のView
@BindingMethodアノテーション 任意の名前 定義対象のView
@BindingAdapterアノテーション 任意の名前 引数のView

これらのうち、下3つについて簡単に説明します。

public setterの属性化

DataBindingではViewに定義されている全てのpublic setterに属性としてアクセスできます。(hideなものはわかりません)

例えばCustomViewを作るとき、必要なデータをsetterから受け取れるようにしたとします。

CustomView.java
public CustomView extends View {
    

    public void setPerson(Person person) {
        
        invalidate();
    }
}

通常は、このメソッドに対してコードからデータを渡す処理を書くと思います。
しかし、DataBindingでは、publicなsetterは属性化されるという特性を持つため、レイアウトxmlで記述することもできます。

activity_main.xml
<layout>
    <data>
        <variable
            name="person"
            type="foo.bar.Person"/>
    </data>

    <foo.bar.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:person="@{person}"/>

</layout>

こうすることで、1つのデータを複数のViewにそれぞれセットせずに、DataBindingに渡せばOKという一貫した処理にできます。

ただしこの場合、値がnullの場合の処理はCustomViewに委ねられるという点に注意が必要です。(実装次第でぬるぽが起こり得る)

BindingMethod

@BindingMethods及び@BindingMethodを利用することで、任意の属性を既存のメソッドにリンクさせることができます。

例として、Viewにはandroid:onClickという属性が定義されていますが、この属性名onClickに対応するメソッドsetOnClick()Viewには定義されていません。
にも関わらず、android:onClick="@{onClickListener}"は有効です。

これは、デフォルトで定義されているViewBindingAdapterに、属性名とメソッドの対応が定義されているためです。

ViewBindingAdapter.java

@BindingMethods({
    
    @BindingMethod(type = View.class, attribute = "android:onClick", method = "setOnClickListener")
})
public class ViewBindingAdapter {
    
}

このように、DataBindingでは、属性名とメソッド名が一致しないいくつかの属性について、デフォルトのリンク定義がされています。(参照)

BindingAdapter

@BindingMethodは既存のメソッドへのリンクでしたが、@BindingAdapterは実装を定義できます。

以下のTextViewを書いたとします。

activity_main.xml
<layout>
    <data>
        <variable
            name="person"
            type="foo.bar.Person"/>
    </data>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{person}"/>
</layout>

TextViewandroid:textに、Person型の変数を代入しています。
通常、TextViewにはPerson型を受け取るsetText()メソッドは存在していないため、これはビルドエラーとなります。

しかし、ここで@BindingAdapterを利用することで、この属性定義を有効にすることができます。
@BindingAdapterは任意のクラスに定義できます。(競合する場合は、先に見つかったほうが優先されるっぽいです)

CustomTextViewBindingAdapter.java
public class CustomTextViewBindingAdapter {

    @BindingAdapter("android:text")
    public static void setText(TextView textView, Person person) {
        String name = person != null ? person.getName() : null;
        textView.setText(name)
    }
}

@BindingAdapterの定義ルールは、

  • public static なメソッドであること
  • 第一引数を適用対象のViewとすること
  • サブクラスにも適用される
  • 第二引数以降とアノテーションのvalueの属性の順番を合わせること

です。
上の定義では、第一引数がTextViewなので、TextViewとそのサブクラスに適用されます。
第二引数はPerson型なので、android:textPerson型の場合にこのメソッドが呼び出されます。

また、valueには複数の属性を書けるので、

CustomTextViewBindingAdapter.java
@BindingAdapter({
    "firstName",
    "lastName"
}, requireAll = false)
public static void setText(TextView textView, String firstName, String lastName) {
    String name = String.format(%s%s, 
            lastName != null ? lastName : "",
            firstName != null ? firstName : "");

    textView.setText(name);
}

こんな書き方もできます。
requireAlltrueの場合は、指定した属性定義されている場合だけ呼ばれます(デフォルトtrue

android:以外を書くか、または何も書かない場合はapp:namespaceに割り当てられます。

デフォルトのTextViewBindingAdapterでは、addTextChangedListener(TextWatcher)の各コールバックを分割してくれているので、android:onTextChangedなんて属性が使えたりします。

Tips

イベントハンドリング

公式のサンプルだと、メソッドリファレンス使えるアピールするためかHanlderに定義するみたいなめんどくさいことしてますが、普通にやるんでも大丈夫です。

activity_main.xml
<layout>
    <data>
        <variable
            name="onTextChanged"
            type="android.databinding.adapters.TextViewBindingAdapter.OnTextChanged"/>

        <variable
            name="onOkClick"
            type="android.view.View.OnClickListener"/>
    </data>

    <LinearLayout>

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onTextChanged="@{onTextChanged}"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@android:string/ok"
            android:onClick="@{onOkClick}"/>

    </LinearLayout>

</layout>
MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState)
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        binding.setOnTextChanged((s, start, before, count) -> {
            // TextWatcher.onTextChanged()
        });

        binding.setOnOkClick((v) -> {
            // click ok button
        });
    }
}

Viewid振って自分でsetOnClickListener()するんでも問題ないです。

RecyclerViewにセパレータ入れるやつ

前に書いたやつ

参考

Data Binding Library | Android Developers

[bindingAdapters | Android]
(https://android.googlesource.com/platform/frameworks/data-binding/+/android-7.0.0_r29/extensions/baseAdapters/src/main/java/android/databinding/adapters)

130
110
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
130
110

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?