バインディング アダプター

バインディング アダプターは、値を設定するための適切なフレームワーク呼び出しを行います。一例としては、setText() メソッドを呼び出すなど、プロパティ値の設定があります。別の例として、setOnClickListener() メソッドを呼び出すなど、イベント リスナーを設定することが挙げられます。

データ バインディング ライブラリを使用すると、アダプターを使用して、値を設定するために呼び出されるメソッドの指定、独自のバインディング ロジックの提供、返されるオブジェクトの型を指定できます。

属性値を設定する

バインドされた値が変更されるたびに、生成されるバインディング クラスは、バインディング式を使用してビューのセッター メソッドを呼び出す必要があります。データ バインディング ライブラリで自動的にメソッドを決定することも、メソッドを明示的に宣言するか、メソッドを選択するカスタム ロジックを用意することもできます。

メソッドの自動選択

example という名前の属性の場合、互換性のある型を引数として受け入れるメソッド setExample(arg) がライブラリによって自動的に検出されます。属性の名前空間は考慮されません。メソッドの検索時には属性の名前と型のみが使用されます。

たとえば、android:text="@{user.name}" 式を指定すると、ライブラリは user.getName() から返される型を受け入れる setText(arg) メソッドを探します。user.getName() の戻り値の型が String の場合、ライブラリは String 引数を受け入れる setText() メソッドを探します。式が int を返した場合、ライブラリは int 引数を受け入れる setText() メソッドを検索します。式は正しい型を返す必要があります。必要に応じて戻り値をキャストできます。

データ バインディングは、指定した名前の属性が存在しない場合でも機能します。データ バインディングを使用して、任意のセッターの属性を作成できます。たとえば、サポートクラス DrawerLayout には属性はありませんが、セッターは多数あります。次のレイアウトでは、setScrimColor(int) メソッドと addDrawerListener(DrawerListener) メソッドがそれぞれ app:scrimColor 属性と app:drawerListener 属性のセッターとして自動的に使用されます。

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}">

カスタムのメソッド名を指定する

一部の属性には、名前で照合できないセッターがあります。このような場合、BindingMethods アノテーションを使用して属性をセッターに関連付けることができます。このアノテーションはクラスで使用され、複数の BindingMethod アノテーション(名前が変更されたメソッドごとに 1 つ)を含めることができます。バインディング メソッドは、アプリの任意のクラスに追加できるアノテーションです。

次の例では、android:tint 属性は setTint() メソッドではなく、setImageTintList(ColorStateList) メソッドに関連付けられています。

Kotlin

@BindingMethods(value = [
    BindingMethod(
        type = android.widget.ImageView::class,
        attribute = "android:tint",
        method = "setImageTintList")])

Java

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

通常は、Android フレームワーク クラスのセッターの名前を変更する必要はありません。属性は、一致するメソッドを自動的に検出するための名前規則を使用してすでに実装されています。

カスタム ロジックを提供する

属性によっては、カスタムのバインディング ロジックが必要な場合もあります。たとえば、android:paddingLeft 属性にはセッターが関連付けられていません。その代わり、setPadding(left, top, right, bottom) メソッドが提供されています。BindingAdapter アノテーション付きの静的バインディング アダプター メソッドを使用すると、属性のセッターを呼び出す方法をカスタマイズできます。

Android フレームワーク クラスの属性には、すでに BindingAdapter アノテーションがあります。次の例は、paddingLeft 属性のバインディング アダプターを示しています。

Kotlin

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}

Java

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

パラメータの型は重要です。最初のパラメータにより、属性に関連付けられるビューの型が決まります。2 番目のパラメータにより、指定された属性のバインディング式で受け入れられる型が決まります。

バインディング アダプターは他の種類のカスタマイズにも便利です。たとえば、ワーカー スレッドからカスタム ローダーを呼び出して、画像を読み込むことができます。

次の例に示すように、複数の属性を受け取るアダプターを作成することもできます。

Kotlin

@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

Java

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
  Picasso.get().load(url).error(error).into(view);
}

次の例に示すように、レイアウト内でアダプタを使用できます。@drawable/venueError はアプリ内のリソースを参照しています。リソースを @{} で囲むことで、有効なバインディング式になります。

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

ImageView オブジェクトに imageUrlerror が使用され、imageUrl が文字列、errorDrawable の場合、アダプタが呼び出されます。いずれかの属性が設定されたときにアダプタが呼び出されるようにする場合は、次の例に示すように、アダプタのオプションの requireAll フラグを false に設定します。

Kotlin

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}

Java

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
  if (url == null) {
    imageView.setImageDrawable(placeholder);
  } else {
    MyImageLoader.loadInto(imageView, url, placeholder);
  }
}

バインディング アダプター メソッドは、ハンドラで古い値を受け取ることができます。古い値と新しい値を取得するメソッドは、次の例に示すように、最初に属性の古い値をすべて宣言してから、新しい値を続けます。

Kotlin

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(newPadding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }
}

Java

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

イベント ハンドラは、次の例に示すように、1 つの抽象メソッドを持つインターフェースまたは抽象クラスでのみ使用できます。

Kotlin

@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
        view: View,
        oldValue: View.OnLayoutChangeListener?,
        newValue: View.OnLayoutChangeListener?
) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue)
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue)
        }
    }
}

Java

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    if (oldValue != null) {
      view.removeOnLayoutChangeListener(oldValue);
    }
    if (newValue != null) {
      view.addOnLayoutChangeListener(newValue);
    }
  }
}

このイベント ハンドラはレイアウト内で次のように使用します。

<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

リスナーに複数のメソッドがある場合は、複数のリスナーに分割する必要があります。たとえば、View.OnAttachStateChangeListener には、onViewAttachedToWindow(View)onViewDetachedFromWindow(View) の 2 つのメソッドがあります。このライブラリには、属性とハンドラを区別するための 2 つのインターフェースが用意されています。

Kotlin

// Translation from provided interfaces in Java:
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
    fun onViewDetachedFromWindow(v: View)
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
    fun onViewAttachedToWindow(v: View)
}

Java

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
  void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
  void onViewAttachedToWindow(View v);
}

一方のリスナーを変更するともう一方のリスナーに影響する可能性があるため、いずれかの属性または両方で機能するアダプターが必要になります。アノテーションで requireAllfalse に設定すると、すべての属性にバインディング式を割り当てる必要がないことを指定できます。次の例をご覧ください。

Kotlin

@BindingAdapter(
        "android:onViewDetachedFromWindow",
        "android:onViewAttachedToWindow",
        requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
        val newListener: View.OnAttachStateChangeListener?
        newListener = if (detach == null && attach == null) {
            null
        } else {
            object : View.OnAttachStateChangeListener {
                override fun onViewAttachedToWindow(v: View) {
                    attach.onViewAttachedToWindow(v)
                }

                override fun onViewDetachedFromWindow(v: View) {
                    detach.onViewDetachedFromWindow(v)
                }
            }
        }

        val oldListener: View.OnAttachStateChangeListener? =
                ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener)
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener)
        }
    }
}

Java

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }
                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }

        OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
                R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

上記の例は、View クラスが OnAttachStateChangeListener のセッター メソッドの代わりに addOnAttachStateChangeListener() メソッドと removeOnAttachStateChangeListener() メソッドを使用しているため、やや複雑です。android.databinding.adapters.ListenerUtil クラスはこれらのリスナーを追跡し、バインディング アダプターで削除できるようにします。

オブジェクトの変換

オブジェクトの自動変換

バインディング式から Object が返されると、ライブラリは、プロパティの値の設定に使用されるメソッドを選択します。Object は、選択されたメソッドのパラメータ型にキャストされます。この動作は、次の例に示すように、ObservableMap クラスを使用してデータを保存するアプリで便利です。

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

式の userMap オブジェクトは値を返します。この値は、android:text 属性の値の設定に使用される setText(CharSequence) メソッドにあるパラメータ型に自動的にキャストされます。パラメータの型があいまいな場合は、戻り値の型を式にキャストします。

カスタム変換

場合によっては、特定の型間でのカスタム変換が必要になります。たとえば、ビューの android:background 属性には Drawable が必要ですが、指定された color 値は整数です。次の例では、属性に Drawable が必要ですが、代わりに整数が指定されています。

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Drawable が想定され、整数が返される場合は、intColorDrawable に変換します。変換を行うには、次のように BindingConversion アノテーション付きの静的メソッドを使用します。

Kotlin

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

Java

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

ただし、バインディング式に指定する値の型は一貫している必要があります。次の例に示すように、同じ式内で異なる型を使用することはできません。

// The @drawable and @color represent different value types in the same
// expression, which causes a build error.
<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

参考情報

データ バインディングの詳細については、次のリソースをご覧ください。

サンプル

Codelab

ブログ投稿