もうちょっと詳しくなるKotlin Android ExtensionsでのView Binding

# はじめに

こんにちは。この記事は、[Android #2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/android2) の10日目の記事です。

みなさんは、[Kotlin Android Extensions](https://kotlinlang.org/docs/tutorials/android-plugin.html)のView Binding機能、使ったことがあるでしょうか。この記事では、代表的な使い方から始まり、どのような仕組みで動いているかや、`Activity` や `Fragment` 以外から使う方法、[Data Binding](https://developer.android.com/topic/libraries/data-binding/)や[Butter Knife](https://github.com/JakeWharton/butterknife)と比べての特徴など、Kotlin Android Extensionsについてもう少し詳しくなるための色々を紹介します。

# Kotlin Android Extensionsとは

[Kotlin Android Extensions](https://kotlinlang.org/docs/tutorials/android-plugin.html)は、KotlinでのAndroid開発体験の向上を目的に提供されているKotlinコンパイラプラグインです。2018年12月現在、以下の2つの機能を持っています。

- View Binding
- `Parcelable` 実装の自動生成

この記事では、1つ目のView Bindingについて説明します。2つ目の `Parcelable` について気になる方は、[Kotlin Android ExtensionsのParcelableSupportを使う](https://qiita.com/sakuna63/items/73b166e0396b6074820d)などを参照してください。

## Kotlin Android Extensionsのセットアップ

Kotlin Android Extensionsは、Gradleスクリプトにおいて `kotlin-android-extensions` プラグインを適用すると使うことができます。Android Studioで、Kotlinを有効にしてアプリプロジェクトを作成すると、実はデフォルトで有効になっています。

```groovy:app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' // <- これ
```

上記したView Bindingや `Parcelable` サポートの機能を使わないのであれば、不要なプラグインなので、逆に削除したほうがいいでしょう。

# View Bindingで何ができるのか

View Binding機能は、`findViewById()` を使わずにActivityやFragmentのプロパティとしてViewのインスタンスを取得できる機能です。例えば、次のようなViewがレイアウトXMLにあった場合に

```xml:activity_main.xml
<TextView
  android:id="@+id/text_view"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" />
```

ActivityやFragmentに、View IDと同じ `text_view` という名前のプロパティが生成され、`findViewById()` を使わなくてもViewのインスタンスにアクセスすることができるようになります。ここで、`text_view` は `TextView` 型のプロパティになっているので、キャストせずに `TextView` として使うことができます。

```kotlin:MainActivity.kt
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // val text_view: TextView というプロパティが存在するかのように使える
        text_view.text = "Hello World!"
    }
}
```

# どのような仕組みになっているのか

魔法のようなKotlin Android Extensionsですが、仕組みはいたって簡単です。`MainActivity` のバイトコードをデコンパイルすると、すぐにわかります。

```java:MainActivity.decompiled.java
public final class MainActivity extends AppCompatActivity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131296284);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.text_view);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "text_view");
      var10000.setText((CharSequence)"Hello World!");
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }
}
```

自動生成されている `_$_findCachedViewById()` というメソッドが、Viewのインスタンスを取得している処理です。`_$_findViewCache` フィールドに持っている `HashMap` をキャッシュとして使いつつ、`findViewById()` しているだけ、ということがわかると思います。

```java:MainActivity.decompiled.java
   private HashMap _$_findViewCache;

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }

      return var2;
   }
```

`onCreate()` 内で `text_view` プロパティにアクセスしていた箇所は、`_$_findCachedViewById(id.text_view)` を呼び出して `TextView` にキャストする処理に変換されています。

```java:MainActivity.decompiled.java
      TextView var10000 = (TextView)this._$_findCachedViewById(id.text_view);
```

つまり、Kotlin Android Extensionsは、View ID名でのプロパティアクセスを、`findViewById()` + `HashMap` によるキャッシュ + キャストという処理に自動変換してくれる仕組みです。

## キャッシュに関するプラグインオプション

上記の例では、`HashMap` がViewのキャッシュに使われていますが、プラグインのオプションを変更して、`SparseArray` を使うように指定したり、キャッシュをさせないようにすることも可能です。

```groovy:app/build.gradle
androidExtensions {
    defaultCacheImplementation = "SPARSE_ARRAY" // HASH_MAP (default), SPARSE_ARRAY, or NONE
}
```

また、[Kotlin Android ExtensionsをExperimentalモードにする](https://kotlinlang.org/docs/tutorials/android-plugin.html#experimental-mode)と、`@ContainerOptions` アノテーションを使って、クラスごとに設定を変えることもできます。

```kotlin:MainActivity.kt
@ContainerOptions(cache = CacheImplementation.SPARSE_ARRAY)
class MainActivity : AppCompatActivity() {
```

# どこでView Bindingの生成プロパティが使えるのか

自動生成されるプロパティは、以下のクラスとその派生クラスで利用することができます ([プラグインのソースコード](https://github.com/JetBrains/kotlin/blob/96bf749d4d2ec5a07b44d0ae10e5ab2b2459c8a5/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/codegen/AndroidContainerType.kt#L25-L33)にリストが定義されています)。

- Activity
  - `android.app.Activity`
  - `android.support.v4.app.FragmentActivity`
  - `androidx.fragment.app.FragmentActivity`
- Fragment
  - `android.app.Fragment`
  - `android.support.v4.app.Fragment`
  - `androidx.fragment.app.Fragment`
- Dialog
  - `android.app.Dialog`
- View
  - `android.view.View`
- LayoutContainer
  - `kotlinx.android.extensions.LayoutContainer` (後述)

Activity, Fragment, View, LayoutContainerのKotlin派生クラスであれば、プロパティアクセス時に、前節で説明した `HashMap` のキャッシュの仕組みが使われます。Dialogや、Javaで実装されたクラスの場合は、キャッシュの仕組みは差し込まれず、プロパティにアクセスすると、単に `findViewById()` が呼び出されます。

Kotlin Android Extensionsを使うようなアプリプロジェクトでは、ActivityやFragmentは基本的にKotlinで実装した派生クラスだと思いますので、ActivityやFragmentのプロパティとしてアクセスする分には、全てキャッシュがきくと考えて問題ありません。

一方、Viewは、Kotlinで実装したカスタムViewであればキャッシュの仕組みが差し込まれるのですが、AndoridプラットフォームやAndroidXライブラリ (サポートライブラリ) のViewの場合は、コンパイル対象でなかったりJava実装だったりするので、キャッシュがききません。

`findViewById()` は時間のかかる処理なので、複数回アクセスする可能性のあるViewに対してはキャッシュがきく方法でインスタンスを取得するか、一度取得したインスタンスを自分で保持しておくことが望ましいです。

## プロパティが生成されるパッケージ

Viewにアクセスするためのプロパティは、次のパッケージに生成されます:

- Activity, Fragment, Dialog, LayoutContainer: `kotlinx.android.synthetic.<ソースセット名>.<レイアウト名>`
- View: `kotlinx.android.synthetic.<ソースセット名>.<レイアウト名>.view`

生成されたプロパティはAndroid Studioの補完の候補となり、補完を使うとインポート文も自動的に挿入されるので、基本的にはインポートについて意識を配る必要はありません。ただ、複数のレイアウトXMLで同じView IDを使っている場合、補完時に間違った候補を選択して、意図と異なるインポート文を挿入してしまう可能性があるので、インポートしているパッケージがあっているか注意する必要があります。

# RecyclerView (ViewHolder) でのKotlin Android Extensionsの活用方法

Activity, Fragment, Dialog, View以外のクラスでも、`LayoutContainer` を実装したクラスであれば、Android Kotlin Extensionsが生成するプロパティをキャッシュつきで利用することができます。これは特に、[`RecyclerView.ViewHolder`](https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.ViewHolder) を実装するときに役にたちます。

例えば、各アイテムが、次のように `ImageView` と `TextView` を持つ `RecyclerView` を実装することを考えてみましょう。

```xml:list_item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:gravity="center_vertical"
  android:minHeight="?attr/listPreferredItemHeight"
  android:orientation="horizontal">

  <ImageView
    android:id="@+id/icon"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="16dp" />

  <TextView
    android:id="@+id/title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="16dp" />

</LinearLayout>
```

Kotlin Android Extensionsを使わない場合、`ViewHolder` と `Adapter.onBindViewHolder()` の実装は次のようになるでしょうか。

```kotlin:MyViewHolder.kt
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val icon: ImageView = itemView.findViewById(R.id.icon)
    val title: TextView = itemView.findViewById(R.id.title)
}
```

```kotlin:MyAdapter.kt
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = ...
        holder.icon.setImageResource(item.icon)
        holder.title.text = item.title
    }
```

## ダメな例

まずはダメな例です。次のように、`MyViewHolder` のプロパティを削除して、代わりに、Kotlin Android Extensionsが `ViewHolder.itemView` 対して生成するプロパティを使うことを考えてみます。

```kotlin:MyViewHolder.kt
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
```

```kotlin:MyAdapter.kt
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = ...
        holder.itemView.icon.setImageResource(item.icon)
        holder.itemView.title.text = item.title
    }
```

一見すると、Kotlin Android Extensionsの機能で `MyViewHolder` の定義がスッキリしており、よさそうに見えます。ただ、前節で説明したように、`View` に対して生成されるプロパティを使うとキャッシュが利用されず、プロパティにアクセスするたびに `findViewById()` が呼び出されてしまいます。これでは、`ViewHolder` というクラスが用意されている意味がありません。

## 惜しい例

毎回、`findViewById()` が呼び出されてしまうのがダメなのであれば、次のように、ViewHolderの生成時にViewのインスタンスを取得して、保持しておけばよさそうです。

```kotlin:MyViewHolder.kt
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val icon = itemView.icon
    val title = itemView.title
}
```

```kotlin:MyAdapter.kt
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = ...
        holder.icon.setImageResource(item.icon)
        holder.title.text = item.title
    }
```

これなら、各Viewに対して `findViewById()` は一度ずつしか呼ばれておらず、問題なさそうです。Kotlin Android Extensionsを使わない場合の例と比較すると、`MyViewHolder` の実装において、`findViewById()` 呼び出しが `itemView.xxx` に差し替わっている点が差分であり、コードを少しスッキリできたでしょうか。ただ、`MyViewHolder` にViewのプロパティを列挙しなければならないことは変わっておらず、ActivityやFragmentが享受しているメリットと比べると、少し残念な感じです。

## LayoutContainerを使う例

そこで、`LayoutContainer` の登場です。`LayouContainer` は、[Kotlin Android ExtensionsをExperimentalモードにする](https://kotlinlang.org/docs/tutorials/android-plugin.html#experimental-mode)と利用することができます。Experimentalとついていますが、一年以上前からExperimentalなのと、非常に単純なインターフェースなので、この後、破壊的な変更が入る可能性は低いと考えています。

```groovy:app/build.gradle
androidExtensions {
    experimental = true
}
```

`LayoutContainer` を使うと、Viewのプロパティを明示的に列挙する必要がなくなり、`MyViewHolder` の実装を次のように簡略化することができます。

```kotlin:MyViewHolder.kt
class MyViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer
```

先述したように、`LayoutContainer` を実装したクラスでは、Kotlin Android Extensionsが生成するプロパティ (キャッシュつき) が利用可能です。そのため、`MyViewHolder` にViewのプロパティを列挙しなくても、`icon` や `title` といったプロパティを使うことができます。

```kotlin:MyAdapter.kt
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = ...
        holder.icon.setImageResource(item.icon)
        holder.title.text = item.title
    }
```

なお、この例で、`MyViewHolder` のコンストラクタの引数が少し変わっていることに気が付いたでしょうか？ `LayoutContainer` は次のような定義を持っており、検索対象としたいViewを含む親Viewを `containerView` プロパティから返す必要があります。生成プロパティにアクセスされると、`containerView` に対して `findViewById()` が呼び出され、指定されたView IDを持つViewを探索する仕組みになっています。

```kotlin:LayoutContainer.kt
public interface LayoutContainer {
    public val containerView: View?
}
```

`MyViewHolder` では、コンストラクタでこのプロパティを実装しており、コンストラクタに渡されたView (= `itemView`) を `containerView` として使うようにしています。`ViewHolder` ではほぼ100%、このパターンでの実装になると思います。

# Kotlin Android Extensionsの特徴と使い所

以上が、Kotlin Android ExtensionsのView Binding機能の説明です。最後に、機能が重複している[Data Binding](https://developer.android.com/topic/libraries/data-binding/)や[Butter Knife](https://github.com/JakeWharton/butterknife)と比較しての、Kotlin Android Extensionsの特徴や使い所をまとめます。

## Viewのインスタンス取得に特化している

ここまで見てきたように、Kotlin Android ExtensionsのView Binding機能は、あくまで `findViewById()` を簡単にする仕組みです。Data BindingのようにViewのプロパティのバインドはできませんし、Butter Knifeにあるような `OnClickListener` などのリスナーのバインドもできません。Viewのインスタンス取得に特化していると言えます。

## 追加コードが不要

`LayoutContainer` が必要なケースは別ですが、ActivityやFragmentから使うケースであれば、追加コードは一切必要なく、これまで通り `Activity.setContentView()` や `Fragment.onCreateView()` をしただけで、XMLに定義されているViewが全てIDでアクセスできるようになります。Data Bindingのようにレイアウト展開時に `ViewDataBinding` の派生クラスを生成したり、Butter KnifeのようにViewを保持するプロパティを自分で定義したりする必要がなく、手軽です。

## コンパイラプラグインによる開発者体験

Kotlin Android ExtensionsはKotlinコンパイラプラグインであり、IntelliJプラグイン、Gradleプラグイン、コード生成器がセットで提供されているソリューションです。以下のように、Android Studioにおける開発者体験がとてもいいです。

- レイアウトXMLを変更すると、アプリを再ビルドしなくても、すぐに変更後のXMLの内容でKotlinコードの補完がきくようになる
　　![Dec-09-2018 23-45-37.gif](https://qiita-image-store.s3.amazonaws.com/0/80557/6290a177-6b14-33c5-6db9-b80b17bf198d.gif)
- レイアウトXMLにおいて、View IDの名前をリファクタ機能で変更すると、同名の生成プロパティを使っていたKotlinコードもあわせて変更される
　　![Dec-09-2018 23-52-33.gif](https://qiita-image-store.s3.amazonaws.com/0/80557/cb4eba84-78b5-4234-2518-b2ec971dab62.gif)
- Kotlinコードの生成プロパティを使っている箇所から、レイアウトXMLにジャンプできる
　　![Dec-09-2018 23-50-29.gif](https://qiita-image-store.s3.amazonaws.com/0/80557/cffd1d16-beaa-303d-10f5-d3ee461526c4.gif)

Data Bindingは、レイアウトXMLからJavaクラスを生成し、生成されたクラスをアプリコードから使う形になるためか、上記のことはいずれもできません。機能の規模が異なるツールなので単純に比較はできませんが、開発者体験という意味では、Kotlin Android Extensionsのほうが気持ちいいです。

## 使い所

- Kotlin Android Extensionsは小さく優れたツールです。Butter KnifeやData BindingをViewのインスタンス取得のためだけに用いているなら、Kotlin Android Extensionsに移行するのは良い選択肢になると思います
- Data Bindingの機能が必要な場合は、Data Bindingを使わざるをえません。その場合、Viewインスタンス取得もData Bindingでことが足りるので、Kotlin Android Extensionsを使う必要はなさそうです
- 小規模なサンプルアプリなど、単純なUIでコードのシンプルさを優先したい場合や、短期間で素早くアプリを開発する必要がある場合は、Kotlin Android Extensionsが役にたつと思います

## 参考: Jakeさんの意見

Butter Knifeの作者でもあるJake Whartonさんは、Kotlin Android Extensionsは言語機能を乱用している、Data Bindingのコード生成が広い点で優れている、という意見のようです。Data Bindingのアプローチのほうが基本的に優れている、というのは分かりますが、"no gain" は若干言い過ぎな気がします...
https://twitter.com/JakeWharton/status/1038085579622305793
> They're super weird. Abuse of the language for no gain. Databinding's codegen is vastly superior but sadly it's tied to databinding.