はじめに
こんにちは。この記事は、Android #2 Advent Calendar 2018 の10日目の記事です。
みなさんは、Kotlin Android ExtensionsのView Binding機能、使ったことがあるでしょうか。この記事では、代表的な使い方から始まり、どのような仕組みで動いているかや、Activity
や Fragment
以外から使う方法、Data BindingやButter Knifeと比べての特徴など、Kotlin Android Extensionsについてもう少し詳しくなるための色々を紹介します。
Kotlin Android Extensionsとは
Kotlin Android Extensionsは、KotlinでのAndroid開発体験の向上を目的に提供されているKotlinコンパイラプラグインです。2018年12月現在、以下の2つの機能を持っています。
- View Binding
-
Parcelable
実装の自動生成
この記事では、1つ目のView Bindingについて説明します。2つ目の Parcelable
について気になる方は、Kotlin Android ExtensionsのParcelableSupportを使うなどを参照してください。
Kotlin Android Extensionsのセットアップ
Kotlin Android Extensionsは、Gradleスクリプトにおいて kotlin-android-extensions
プラグインを適用すると使うことができます。Android Studioで、Kotlinを有効にしてアプリプロジェクトを作成すると、実はデフォルトで有効になっています。
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にあった場合に
<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
として使うことができます。
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
のバイトコードをデコンパイルすると、すぐにわかります。
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()
しているだけ、ということがわかると思います。
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
にキャストする処理に変換されています。
TextView var10000 = (TextView)this._$_findCachedViewById(id.text_view);
つまり、Kotlin Android Extensionsは、View ID名でのプロパティアクセスを、findViewById()
+ HashMap
によるキャッシュ + キャストという処理に自動変換してくれる仕組みです。
キャッシュに関するプラグインオプション
上記の例では、HashMap
がViewのキャッシュに使われていますが、プラグインのオプションを変更して、SparseArray
を使うように指定したり、キャッシュをさせないようにすることも可能です。
androidExtensions {
defaultCacheImplementation = "SPARSE_ARRAY" // HASH_MAP (default), SPARSE_ARRAY, or NONE
}
また、Kotlin Android ExtensionsをExperimentalモードにすると、@ContainerOptions
アノテーションを使って、クラスごとに設定を変えることもできます。
@ContainerOptions(cache = CacheImplementation.SPARSE_ARRAY)
class MainActivity : AppCompatActivity() {
どこでView Bindingの生成プロパティが使えるのか
自動生成されるプロパティは、以下のクラスとその派生クラスで利用することができます (プラグインのソースコードにリストが定義されています)。
- 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
を実装するときに役にたちます。
例えば、各アイテムが、次のように ImageView
と TextView
を持つ RecyclerView
を実装することを考えてみましょう。
<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()
の実装は次のようになるでしょうか。
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val icon: ImageView = itemView.findViewById(R.id.icon)
val title: TextView = itemView.findViewById(R.id.title)
}
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
対して生成するプロパティを使うことを考えてみます。
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
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のインスタンスを取得して、保持しておけばよさそうです。
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val icon = itemView.icon
val title = itemView.title
}
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モードにすると利用することができます。Experimentalとついていますが、一年以上前からExperimentalなのと、非常に単純なインターフェースなので、この後、破壊的な変更が入る可能性は低いと考えています。
androidExtensions {
experimental = true
}
LayoutContainer
を使うと、Viewのプロパティを明示的に列挙する必要がなくなり、MyViewHolder
の実装を次のように簡略化することができます。
class MyViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer
先述したように、LayoutContainer
を実装したクラスでは、Kotlin Android Extensionsが生成するプロパティ (キャッシュつき) が利用可能です。そのため、MyViewHolder
にViewのプロパティを列挙しなくても、icon
や title
といったプロパティを使うことができます。
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を探索する仕組みになっています。
public interface LayoutContainer {
public val containerView: View?
}
MyViewHolder
では、コンストラクタでこのプロパティを実装しており、コンストラクタに渡されたView (= itemView
) を containerView
として使うようにしています。ViewHolder
ではほぼ100%、このパターンでの実装になると思います。
Kotlin Android Extensionsの特徴と使い所
以上が、Kotlin Android ExtensionsのView Binding機能の説明です。最後に、機能が重複しているData BindingやButter Knifeと比較しての、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コードの補完がきくようになる
- レイアウトXMLにおいて、View IDの名前をリファクタ機能で変更すると、同名の生成プロパティを使っていたKotlinコードもあわせて変更される
- Kotlinコードの生成プロパティを使っている箇所から、レイアウトXMLにジャンプできる
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.