blog.bouzuya.net

2019-09-03 BindingAdapter に (i: Int) -> Unit を渡せない

疲れている。朝は頭痛がひどかったが落ち着いてきた。


Data Binding の Listener のことを書いたつもりになっていたけどメモ止まりだった。 ↓に貼っておく。供養。

(item: T) -> Unit みたいなのが出てくるとまた動かなくなるように思う。


Data Binding で BindingAdapter に (i: Int) -> Unit のような Kotlin の関数をうまく渡せない。

↓のような ViewModel があるとする。 Data Binding で click を呼び出したい。

class MyViewModel : ViewModel() {
  fun click(i: Int) {
    // do something
  }
}

これを簡単に設定するための BindingAdapter を自作しよう。まず動かない例。

@BindingAdapter("onItemSelected")
fun Spinner.setOnItemSelected(listener: ((i: Int) -> Unit)?) {
    if (listener == null) return
    onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            listener(position)
        }

        override fun onNothingSelected(p0: AdapterView<*>?) {
            // do nothing
        }
    }
}
<Spinner
  app:onItemSelected="@{(i) -> viewModel.click(i)}"
  ...
  />

動きそうな見た目だけど動かない。

@{(i) -> viewModel.click(i)} の型は Binding Adapter の (i: Int) -> Unit の型にあってくれない。

[databinding] {"msg":"cannot find method click(java.lang.Object) in class ...

こんなエラーになる。どうも ijava.lang.Object (Kotlin の Any) として扱われている。推論してくれない。

@{viewModel.click}@{viewModel::click} だと……

[databinding] {"msg":"Listener class kotlin.jvm.functions.Function1 with method invoke did not match signature of any method viewModel.click", ...

こんなエラーになる。おそらく Binding Adapter の (i: Int) -> Unit か ViewModel の click(i: Int): Unit のどちらかの Function1 から型が落ちてあわないのだろう……。

さて解決策。

まず Binding Adapter と ViewModel を (o: Any) -> Unit にすれば通るけどダウンキャストが必要になるのであんまりだ。

結局のところ 1 メソッドの interface を定義するのが一番良さそうだった。

interface OnItemSelectedListener {
    fun onItemSelected(i: Int)
}

@BindingAdapter("onItemSelected")
fun Spinner.setOnItemSelected(listener: OnItemSelectedListener?) {
    if (listener == null) return
    onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            listener.onItemSelected(position)
        }

        override fun onNothingSelected(p0: AdapterView<*>?) {
            // do nothing
        }
    }
}

これなら @{(i) -> viewModel.click(i)} でも @{viewModel.click} でも @{viewModel::click} でも interface の型にあってくれる。

ぼくは Java の lambda の挙動に詳しくないのでここからは推測。 Java の lambda はおそらく 1 メソッドの interface を実装する形に変換されるのだろう。そして Data Binding の @{() -> ...} は Java の lambda に似せているのだろう。そう考えると 1 メソッドの interface を面倒でも定義するのが良い方法なのだと思う。知らんけど。