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

バインディング アダプターは、各種の値を設定するのに適したフレームワークを呼び出します。たとえば、プロパティ値の設定(setText() メソッドの呼び出しなど)や、イベント リスナーの設定(setOnClickListener() メソッドの呼び出しなど)を行います。

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

属性値の設定

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

メソッドの自動選択

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

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

データ バインディングは、指定した名前の属性が存在しない場合でも機能します。さらに、データ バインディングを使用してセッターの属性を作成できます。たとえば、サポートクラス DrawerLayout には属性はありませんが、各種のセッターはあります。次のレイアウトでは、setScrimColor(int) メソッドと setDrawerListener(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());
    }

    

パラメータの型は重要です。1 つ目のパラメータには、属性に関連付けられているビューの型を指定します。2 つ目のパラメータには、所定の属性のバインディング式で受け入れられる型を指定します。

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

競合が存在する場合、自分で定義したバインディング アダプターによって Android フレームワークから提供されるデフォルトのアダプターがオーバーライドされます。

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

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}" />
    

アダプターは、imageUrlerror の両方が ImageView オブジェクトで使用されており、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(padding,
                        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);
    }
    

1 つのリスナーを変更すると他のリスナーにも影響することがあるため、属性のいずれかまたは両方で使用できるアダプターが必要になります。次の例に示すように、アノテーションで 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 クラスで以前のリスナーを追跡できるため、バインディング アダプターでは削除してもかまいません。

インターフェース OnViewDetachedFromWindowOnViewAttachedToWindow@TargetApi(VERSION_CODES.HONEYCOMB_MR1) アノテーションを付けることで、データ バインディングのコード生成ツールが、Android 3.1(API レベル 12)以降(addOnAttachStateChangeListener() メソッドでサポートされているのと同じバージョン)で実行されている場合にのみリスナーを生成する必要があることを認識できます。

オブジェクトの変換

オブジェクトの自動変換

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);
    }

    

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

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

参考情報

データ バインディングの詳細については、以下の参考情報をご確認ください。

サンプル

Codelab

ブログ投稿