BindingAdapterに関するいくつかのこと
シンプルな方法でビューに値をセットする。
Created at Updated at

2026 Words
⚠️

追記 (2020-11-30)

双方向バインディングのやり方を書いてなかったので追記。

#双方向バインディング

拡張関数かオブジェクトかの選択

以下2通りの方法でバインディングアダプタを記述することができるが、基本はオブジェクトの方を使った方がいいと思う。
コード側でも共用したい場合に限り拡張関数の方を使う感じで。

拡張関数として用意する

@BindingAdapter("items")
fun RecyclerView.setItems(items: List<Item>?) {
    // 省略
}

オブジェクトとして用意する

object RecyclerViewBindingAdapters {
    @JvmStatic
    @BindingAdapter("items")
    fun setItems(view: RecyclerView, items: List<Item>?) {
        // 省略
    }
}

DataBinding

データバインディングを使うと色々嬉しいですねという話。導入は前書いた。

Android - DataBindingはじめ - すいはんぶろぐ.io

前記事でBindingAdapterを使う方法を書いてなかったのでここに記録しておく。

以下の内容は自分がやったことのみ記述していますので、詳細な情報は一次情報にあたってください。

バインディング アダプター  |  Android デベロッパー  |  Android Developers

BindingAdapter

例えば「真偽値をVisibilityに変換して指定する」例。

hoge.kt

@BindingAdapter("android:visibility")
fun View.setBoolToVisibility(b: Boolean?) {
    this.visibility =
        if (b == true) View.VISIBLE
        else View.GONE
}

kotlinの場合、指定する対象にこのような拡張関数を用意するだけでいい。

バインディングアダプタは次のように名前と値の型が符合する属性値を指定した場合にのみ実行される。
アダプタにする拡張関数の引数型は必ずしもnullableである必要はない。しかし、その場合nullが渡された途端に実行時エラーになるので注意がいる。

layout.xml

<!-- これであたかもandroid:visibilityに直接真偽値をセットしているように書ける -->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{vm.hogeBool}"
/>

なお、vm.hogeBoolLiveData<Boolean>とかの場合でもこの書き方でいい("@{vm.hogeBool.value}"とかにしない)

複数の属性を扱う

上記の例ではandroid:visibilityだけをセットしていたが、「複数の属性をすべてセットしないといけないようにする」こともできる。

たとえば「ImageViewに画像ソースのURL文字列を渡したいが、値がnullの場合は非nullなデフォルト値を使用する」みたいな例。

@BindingAdapter("src", "srcDefault")
fun ImageView.setSource(src: String?, defaultSrc: String) {
    Glide.with(context)
        .load(src ?: defaultSrc)
        .into(this)
}

単純に、@BindingAdapterに渡す属性名を増やせばいい。

なおこの方法だと「requireAll = true」、つまり指定したすべての属性値に値がちゃんと指定されていないとコンパイルエラーになる。
あってもなくてもいい属性がある場合は次のようにできる。

@BindingAdapter(value = ["src", "srcDefault"], requireAll = false)
fun ImageView.setSource(src: String?, defaultSrc: String?) {
    val imgSrc = src ?: defaultSrc ?: return
    Glide.with(context)
        .load(imgSrc)
        .into(this)
}

(属性の組み合わせ分だけアダプタを宣言してもいいが、冗長にはなる)

@BindingAdapter("src")
fun ImageView.setSource(src: String?) {
    if (src == null) return
    Glide.with(context)
        .load(src)
        .into(this)
}

@BindingAdapter("src", "srcDefault")
fun ImageView.setSource(src: String?, defaultSrc: String?) {
    val imgSrc = src ?: defaultSrc ?: return
    Glide.with(context)
        .load(imgSrc)
        .into(this)
}

属性名の名前空間

バインディングアダプタのvalueに指定する属性名はここまで@BindingAdapter("hoge")のように記述してきたが、このときこのバインディングアダプタは「属性値の型が一致する “android:“以外のすべての名前空間のhoge」にマッチする。
("app:hoge""hoge"…)

"android:"にマッチさせる為には明示的に@BindingAdapter("android:hoge")と書く必要がある。

値の変更を処理する

属性値がoldValueからnewValueに変更されたときその両方の値を使って何かすることもできる。

@BindingAdapter("hoge")
fun View.setHoge(oldValue: Hoge?, newValue: Hoge?) {
    ...
}

複数属性の場合

@BindingAdapter("a", "b")
fun View.setHoge(oldA: Hoge?, oldB: Hoge?, newA: Hoge?, newB: Hoge?) {
    ...
}

双方向バインディング

Sliderの現在値(android:value)を双方向バインドする例。


object SliderBindingAdapters {

    /** Model --> View */
    @JvmStatic
    @BindingAdapter("android:value")
    fun bindSliderValue(slider: Slider, value: Float?) {
        if (value != null && slider.value != value) {
            slider.value = value
        }
    }

    /** View --> Model */
    @JvmStatic
    @InverseBindingAdapter(attribute = "android:value")
    fun bindSliderValueInverse(slider: Slider) : Float {
        return slider.value
    }

    /**
     * `android:value`が更新されたことを検知するリスナを登録して、
     * 値変更時に`bindSliderValueInverse`を呼び出す
     */
    @JvmStatic
    @BindingAdapter("android:valueAttrChanged")
    fun bindListeners(slider: Slider, valueAttrChanged: InverseBindingListener?) {
        slider.addOnChangeListener { _, _, _ ->
            valueAttrChanged?.onChange()
        }
    }
}
<com.google.android.material.slider.Slider
    android:value="@={vm.sliderValue}"
    android:valueFrom="0.0"
    android:valueTo="1.0"
    android:stepSize="0.02"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

InverseBindingAdapterと、リスナをセットするためのBindingAdapterを書くのが肝。

バインドするリスナの属性名はデフォルトでは(値バインド対象属性名+)AttrChangedとなる。(今回の場合android:valueの変更を監視するからandroid:valueAttrChanged)

任意の属性名を設定したい場合は@InverseBindingAdapterアノテーションの引数にevent="~~~"という形で渡す。

bindListenersで設定しているリスナの中身は、要は値が更新されたことだけ伝えられればいいのでただ単純にvalueAttrChanged?.onChange()を呼んでいる。(InverseBindingListenerが呼ばれる)

See Also