この記事は Retty Advent Calendar 2019 4日目の記事です。
はじめに
Google I/O 2019でViewとコードをBindする新しい方法として、「ViewBinding」が発表されました。🎉
それから時が経ち、半年ほど経過した現在、ようやくこの機能に触れることが出来るようになりました。
今回はこのViewBindingについて紹介してみます。
ViewBindingとは
以下のようにIdが指定されたViewがあるとします。
<TextView android:id="@+id/text_view" />
これに対して、Java/Kotlin側のコードから
binding.textView.text = "Hello! View Binding!"
とアクセスできるようにする仕組みです。 詳細はこちらを参照してください。
上記コードは以下のお馴染みのコードとほぼ同じ挙動になります
val textView = findViewById<TextView>(R.id.text_view) textView!!.text = "Hello! View Binding!" // これはView Bindingではないけど
このViewBindingは大変優れた仕組みで、使わない理由が存在しないくらいなので、明日からタイトル詐欺になってしまいますが、まだβ版の機能なので、stable版AndroidStudio3.6がリリースされたら使っていきましょう🚀 。以下に、その理由をおよそ10年に渡るAndroidのViewとコードの紐付け方法の歴史から述べていきます。
本記事ではfindViewById, Kotlin Android Extensions, DataBinding, そして、ViewBinding、以上四つの紐付け方法について、登場年にしたがって以下の順番で紹介し、ViewBindingの良さを布教しようと思います。
- findViewById(2008年登場)
- Kotlin Android Extensions(2015年登場)
- DataBinding(2015年登場)
- ViewBinding(2019年登場)
findViewById
Androidで昔から使われている方法がこれです。APIレベル1の時からあるおなじみの方法ですね。
val textView = findViewById<TextView>(R.id.text_view) textView!!.text = "Hello! findViewById"
この方法にはいくつかの問題があります
つらいところ😢
コードが冗長で大変
findViewByIdでViewにアクセスしたければ、まずIdでbindするコードを書かなければなりません。XML側でViewに対してIdを定義した後にJava/Kotlin側のコードに戻り、またIdを書いて似たような名前で変数を定義しなければなりません。面倒ですね。
戻り値がnullable
findViewByIdの戻り値はnullable
です。タイミングさえ間違っていなければ基本的にnullではないのですが、ActivityやFragmentが参照しているものではないLayoutのViewのIdを間違えて指定することもあります。このような場合はnullになってしまいます。こういった点も使いにくい点の一つです。
型安全ではない
findViewByIdは型安全ではなく、テンプレート引数やキャストによって型を指定しなければなりません。間違えると実行時にクラッシュしてしまいます。
このように、数多くの辛いところがあり、たくさんのAndroidエンジニアが苦しめられてきました😭。
Kotlin Android Extentions
少し前にRettyアプリでも導入した方法です。
text_view!!.text = "Hello! Kotlin Android Extentions"
このようにViewにアクセスできます。 これはコード生成によって実現されています。 XML側でIdを定義すると、それを元にして、対象のViewをfindViewByIdして取得しキャッシュしてくれる関数をIdの名前でアクセスできるように生成してくれます。
以下にfindViewByIdと比較した時のメリットを並べていきます。
findViewByIdと比較した時のメリット😆
簡潔に書ける
前述の通り、XMLで指定したIdをもとに、findViewByIdするコードを自動的に生成してくれるので、XMLで一度Idさえ定義してしまえば、Java/Kotlin側のコードに似たようなものを何度も書くようなことは不要になりました。
型安全
型安全なので、型を指定する必要もないです。型指定を間違えて実行時になってようやく気づけるつまらない凡ミスから解放されます。
こういったメリットにより、今までfindViewByIdで行なっていた、「Idを書いて、そのIdとほぼ同じ変数を定義して、お決まりのパターンでViewとbindするコードを手作業で実装する」という機械的な作業から解放されました。DX爆上がりです💥。
しかし、これにもまだ以下のような問題があります。
まだつらいところ😥
Java/Kotlinのコードにスネークケースが混ざる
見て分かるとおり、ViewのIdにスネークケース採用している場合、キャメルケースを使うJava/Kotlinのスタイルとあわなくて気持ち悪いです。命名規則を変えてしまえば良いかもしれませんが、もともとXML内のIDの命名はスネークケースで行うのが慣習となっており、特に既存プロジェクトの場合では一筋縄ではいけません、
戻り値がnullable
findViewByIdと同様に、別のLayoutに存在するViewを引っ張ってこようとするコードが書けてしまうといった問題がまだ存在します。もしも間違って別LayoutにあるViewを指定してしまった場合、nullが返ってきてしまいます。これを防ぐために、Rettyアプリ内のidはやたら冗長に命名されている部分があります。
このように、Kotlin Android Extensionsでも解決できない課題が多くありました。
DataBinding
DataBindingは、大雑把に以下のような機能を含んだライブラリです。
- Viewに付与したIdから、バインディング用のクラスを自動で作成し、コードからViewへのアクセスを容易にする機能
- バインディング式と呼ばれる、XMLの中でViewで定義した変数を参照したり簡単なロジックを書いたり出来るようにする機能
本記事ではViewのバインドをどう行うか、というところに焦点を置きたいので、まずは1について言及します。
この1で述べた機能はKotlin Android Extensionsと大変に似ており、text_view
とIdが振られたViewにアクセスするコードは以下のように書けます
binding = DataBindingUtil.setContentView(this, R.layout.activity_main); ~~~ binding.textView.text = "Hello! DataBinding."
以下にDataBindingの良いところを書いていきます。
良いところ😆
簡潔に書ける
Kotlin Android Extensionsと同じです。
型安全
Kotlin Android Extensionsと同じです。
戻り値がnullableではない
nullableじゃないので、findViewById
やKotlin Android Extensions
を使っていたときのように、わざわざnullである可能性を考慮する必要がありません。
ちなみに、なぜnullableでなくても良くなったかと言うと、DataBindingはXML一つから一つのClassを生成するため、Java/Kotlin側のコードから指定するClassさえ間違えていなければ、別のレイアウトに存在するViewを間違えて指定してしまうことが無いためです。
スネークケースのIdをキャメルケースに変換してくれる
Kotlin Android Extensions
の項目で悪いところとして上げた点ですが、これが解決されています。コード生成時に、Idがスネークケースであれば、キャメルケースに変換されるようになっており、Java/Kotlinのコードスタイルとの違和感がないです。
ここまで良いこと尽くめですが、これでもまだ悪いところはあります。
悪いところ😢
ビルドに時間がかかる
ビルド時にコードを生成している関係上、どうしてもビルド時間は長くなります。kapt
を利用しているため、尚更です。
バインディング式の機能があまり役に立たない
これは個人の感想になってしまう部分もあるのですが、XMLの中で変数を参照して値をViewにセットしたり、簡単なロジックが書けてしまう機能自体が良くないと感じる部分があります。
textを表示したりクリックイベントを設定したいくらいの簡単なものなら可能ですが、出来ないこともかなり多いです。そのため、そのままではXMLとコードのどちらにもロジックが分散してしまい、中途半端になりがちです。
そういった部分を解決するために、BindingAdapter
という仕組みもあり、ある属性についてどうXMLで指定した値をViewにセットするのかを、独自に決めて実装をすることも出来ます。しかし、これで出来たものはあくまでチーム内のドメイン知識に過ぎません。これをやり過ぎるとプロジェクトの学習コストが高くなってしまう問題も秘めています。せめてもう少し公式の提供する機能が豊富であれば......と思うところです。
ViewBinding
最後に今回の主役であるViewBindingについてです。 大雑把に言うとDataBindingの「Viewにアクセスしやすくするコード生成機能」だけを抜き出してきたものです。以下のように、コードの見た目も似たような感じです。
binding = ActivityMainBinding.inflate(layoutInflater)
~~~
binding.textView.text = "Hello! ViewBinding."
良いところ😆
簡潔に書ける
Kotlin Android Extensions、DataBindingと同じです
型安全
Kotlin Android Extensionss、DataBindingと同じです
戻値がnullableではない
DataBindingと同じです
ビルドがDataBinding比で早い⚡
同じコード生成でも、DataBindingはkaptを利用しているのでとても遅いです。 ViewBindingはシンプルに作られている分、kaptに依存していないのでビルドが速いです。
このように、ViewBindingは今までのViewをBindする様々な方法につきものの課題を大体解決する素晴らしい方法であることが解ると思います。
ここまでを表に纏めると以下のようになります。
特徴\bind方法 | findViewById | Kotlin Android Extensions | DataBinding | View Binding |
---|---|---|---|---|
Idを二回書かなくていい | X | O | O | O |
型安全 | X | O | O | O |
not nullable | X | X | O | O |
ビルド速度 | O | △ | X | △ |
ViewBindingの素晴らしさが見えてきますね🌈。 次は、そんなViewBindingをどうやって使うのか紹介します。
ViewBindingの使い方
簡単です。AndroidStudio3.6(Canary11以降)を導入し、モジュールのbuild.gradleに以下を追記。
android {
~~~
viewBinding {
enabled = true
}
}
あとはこんな感じで使えます。
private lateinit var binding: ActivityMain override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) binding =ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) } ~~~ binding.textView.text = "Hello! View Binding!"
ViewBindingの仕組み
このようなXMLがあるとします。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
このXMLから、こんな感じのコードが生成されます。
// Generated by view binder compiler. Do not edit! package com.example.helloviewbinding.databinding; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.viewbinding.ViewBinding; import com.example.helloviewbinding.R; import java.lang.NullPointerException; import java.lang.Override; import java.lang.String; public final class ActivityMainBinding implements ViewBinding { @NonNull private final ConstraintLayout rootView; @NonNull public final TextView textView; private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull TextView textView) { this.rootView = rootView; this.textView = textView; } @Override @NonNull public ConstraintLayout getRoot() { return rootView; } @NonNull public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) { return inflate(inflater, null, false); } @NonNull public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, boolean attachToParent) { View root = inflater.inflate(R.layout.activity_main, parent, false); if (attachToParent) { parent.addView(root); } return bind(root); } @NonNull public static ActivityMainBinding bind(@NonNull View rootView) { // The body of this method is generated in a way you would not otherwise write. // This is done to optimize the compiled bytecode for size and performance. String missingId; missingId: { TextView textView = rootView.findViewById(R.id.text_view); if (textView == null) { missingId = "textView"; break missingId; } return new ActivityMainBinding((ConstraintLayout) rootView, textView); } throw new NullPointerException("Missing required view with ID: ".concat(missingId)); } }
見ての通り、非常にシンプルで、bindメソッドにRootViewを渡すと、そのRootViewから欲しいViewをfindViewByIdで引っ張ってきてBindingクラスのメンバ変数に保持しているだけです。 いつも手で書いているコードを機械が書いてくれるようになったような感じです。
おわりに
ViewBindingを使うことで、大きなデメリット無く、
- 機械的に書いている冗長なコード
- nullable
- キャストや型指定必須
といったつらみから開放されることとなりました。使わない理由が無いくらいなので、どんどん使っていきましょう。とりあえず、AndroidStudio3.6がリリースされたらRettyのAndroidアプリ開発においても活用していく予定です。
ViewBindingの紹介をやっておいてこれを言うのもアレですが、そもそもXMLでUI組むのつらいから個人的にはJetpackComposeで宣言的UIしたい思いもあります。 でもまだアルファなので、当分先になりそうです。その時が来るまではViewBindingで頑張っていきましょう!💪