KotlinとData Bindingを組み合わせた場合の問題点と解決法

これはKotlin Advent Calendar 2016の3日目の記事です。

はじめに

KotlinとData Binding、両方とも便利ですよね。

この記事ではこれらを組み合わせて使った場合に発生するいくつかの問題点と解決法を紹介したいと思います。ググるといくつか情報が出てきますが、断片的であったり新しい解決法が登場していたりするので、情報を集約して解決法を列挙する形で書いています。

この記事では、

  1. Kotlinから自動生成されたクラスが参照できない
  2. BindingAdapterを使ったカスタムバインディングが使えない

という2つの問題を扱います。

1. Kotlinから自動生成されたクラスが参照できない

これはData Bindingに限った話ではなく、KotlinとAnnotation Processingを組み合わせた場合に発生します。

Data Bindingを使う場合、XMLで記述したレイアウトをLayoutタグで囲むことでFooBindingのようなクラスが生成されます。しかし、このFooBindingをKotlinから参照すると正常にビルドが出来ず、以下のようなビルドエラーが表示されると思います。

Error:(7, 35) Unresolved reference: databinding
Error:(19, 27) Unresolved reference: ItemArticleBinding
Error:(40, 35) Unresolved reference: ItemArticleBinding

この記事で詳しくは説明しませんが、KotlinとJavaコンパイル順序が関係しています。以下の記事で詳しい解説があるので、興味のある方はご覧ください。

この問題を解決する方法として、2016年12月現在では2つの方法があります。

generateStubsを有効にする

build.gradleに以下の記述を追加することでKotlinとJavaコンパイル順序に関連した問題が解消されて、KotlinからFooBindingのような自動生成されたクラスが参照できるようになると思います。

kapt {
    generateStubs = true
}

kotlin-kaptを使う

もう1つの解決方法として、build.gradleに以下の記述を追加することも解決できます。

apply plugin: 'kotlin-kapt'

しかし、公式ブログには以下のような記述があり、kotlin-kaptはまだ実験段階のようです。この記事を書くにあたって簡単なサンプルを実装した限りは正常に動作しましたが、まだ問題が残っている可能性もありそうです。

The new annotation processing still has known issues and may not be compatible with all annotation processors. You should enable it only if you’ve run into problems with the default kapt annotation processing implementation.

2. BindingAdapterを使ったカスタムバインディングが使えない

Data BindingではTextViewにテキストをバインドするといった標準で用意されているバインディングに加えて、カスタムバインディングを実装することができます。よくある例としては、ImageViewにGlideやPicassoなどを使ってURLをバインディングするものだと思います。これをJavaで実装すると以下のようになります。

public class CustomBinder {
    @BindingAdapter("app:imageUrl")
    public static void imageUrl(ImageView imageView, String url) {
        Glide.with(imageView.getContext()).load(url).into(imageView);
    }
}

JavaではStaticメソッドにBindingAdapterを付与することでカスタムバインディングを実装することができます。これをKotlinで実装すると以下のようになります。

object CustomBinder {
    @BindingAdapter("app:imageUrl")
    fun imageUrl(imageView: ImageView, url: String?) {
        Glide.with(imageView.context).load(url).into(imageView)
    }
}

これでいけるかと思いきや、実行すると以下のようなエラーが発生します。

java.lang.IllegalStateException: Required DataBindingComponent is null in class ItemArticleBinding. A BindingAdapter in com.yuyakaido.android.flow.presentation.binder.CustomBinder.Companion is not static and requires an object to use, retrieved from the DataBindingComponent. If you don't use an inflation method taking a DataBindingComponent, use DataBindingUtil.setDefaultComponent or make all BindingAdapter methods static.

一言で要約すると、カスタムバインディングはStaticメソッドとして実装してね、という感じです。

この問題を解決する方法はいくつも考えられますが、大きくは2つあります。

JvmStaticアノテーションを付与する

以下のようにJvmStaticアノテーションを付与すると、JavaからはStaticメソッドとして見えるようになります。

object CustomBinder {
    @JvmStatic
    @BindingAdapter("app:imageUrl")
    fun imageUrl(imageView: ImageView, url: String?) {
        Glide.with(imageView.context).load(url).into(imageView)
    }
}

これでめでたくカスタムバインディングが動作するはずです。

カスタムバインディングを拡張関数として実装する

もっとKotlinらしく書くとしたら、カスタムバインディングを拡張関数として実装してもいいかもしれません。

@BindingAdapter("android:imageUrl")
fun ImageView.imageUrl(url: String?) {
    Glide.with(context).load(url).into(this)
}

上記のようにImageViewの拡張関数を実装すると、以下のようにカスタムバインディングであることを意識せずにバインドできるようになります。

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:imageUrl="@{article.thumbnail()}"/>

さいごに

この記事ではKotlinとData Bindingを組み合わせて使う場合の問題点と解決策を紹介しました。

それでは、良いKotlin×Data Bindingライフを!