Hello ViewBinding! 歴史から学ぶ明日からViewBindingを使うべき理由

この記事は Retty Advent Calendar 2019 4日目の記事です。

qiita.com

はじめに

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!"

とアクセスできるようにする仕組みです。 詳細はこちらを参照してください。

developer.android.com

上記コードは以下のお馴染みのコードとほぼ同じ挙動になります

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の良さを布教しようと思います。

  1. findViewById(2008年登場)
  2. Kotlin Android Extensions(2015年登場)
  3. DataBinding(2015年登場)
  4. 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は、大雑把に以下のような機能を含んだライブラリです。

  1. Viewに付与したIdから、バインディング用のクラスを自動で作成し、コードからViewへのアクセスを容易にする機能
  2. バインディング式と呼ばれる、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じゃないので、findViewByIdKotlin 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で頑張っていきましょう!💪

参考

developer.android.com

developer.android.com developer.android.com

kotlinlang.org