Boost」など、さまざまなカンファレンスを企画・運営しています。BOOKS(コードジン・ブックス)は、CodeZineの連載からカットアップした、開発現場の課題解決に役立つ書籍シリーズです。Studio2で始めるアプリ開発入門」連載、および『Androidアプリ開発の教科書』の続編にあたる内容として、Jetpackを取り上げていきます。前回は、LiveDataを自動生成できるRoomの機能と、そのLiveDataと同等の機能を提供できるKotlin専用のFlowも紹介しました。今回から3回にわたって紹介する内容は、画面へのデータ反映処理を自動化できるデータバインディングです。今回は、データバインディングの前編として、ビューバインディングから話を始め、データバインディングの基本的な利用方法を紹介します。画面へのデータの反映はバインディングオブジェクトに直接セットフラグメントでの利用まとめ画面部品の取得を自動化できるビューバインディング今回のテーマは、画面へのデータ反映処理を自動化できるデータバインディングです。そのデータバインディングを紹介する前に、画面へデータを反映させる従来の方法から話を始め、ビューバインディングを紹介します。今回のサンプルデータは、GitHubから参照できます。図1のようなシンプルなアプリを考えてみます。このアプリは、ボタンをタップすると、乱数を発生させて、それを表示させるものです。乱数の発生や発生した乱数値の保持を第1回で紹介したViewModelに任せるとしても、そのデータを表示させるのはアクティビティの役割であり、従来は、Javaではリスト1の、Kotlinではリスト2の(1)と(2)のコードを記述していました。ボタンのリスナ登録も、(3)と(4)のようなコードを記述していました。(4)ここで注目したいのは、(1)と(3)です。画面部品に対して、データを表示したりリスナを設定したりなど、何か処理を行う場合は、まず、画面部品のidのR値を引数として、findViewById()メソッドで該当画面部品を取得する必要があります。このfindViewById()を利用したコードは、画面部品を利用するたびに記述する必要があり、そのぶん、アクティビティ内のコードが膨らむ傾向があります。コード量の問題だけではなく、nullとデータ型の問題を孕んでいます。idのR値として、現在表示させている画面とは別のレイアウトxmlに記述されたidを指定すると、findViewById()の戻り値はnullとなり、結果的にその後のコード(例えば、(2)や(4))でNullPointerExceptionが発生します。戻り値を格納する変数のデータ型(Kotlinコードならばジェネリクスで指定するデータ型)として違うものを記述すると、ClassCastExceptioinが発生します。findViewById()はnull安全でもなく、型安全でもない、ということです。ビューバインディングとその利用設定このような問題を解決するために、Jetpackではビューバインディングという仕組みが導入されています。このビューバインディングでは、レイアウトxmlファイルの記述を元に、その画面に含まれる画面部品が格納されたオブジェクトを自動生成させる仕組みです。このビューバインディングを利用する場合、まず、プロジェクトに設定が必要です。app)に、リスト3の太字の追記を行います。ファイル直下に既に存在しているandroidブロックにbuildFeaturesブロックを追加し、viewBindingプロパティをtrueとします。:}ビューバインディングの利用コードこれで、プロジェクト内でビューバインディングが利用できるようになります。そのビューバインディングを利用すると、リスト1の画面部品を扱うコードは、Javaではリスト4の(5)や(6)のようになります。(6)}リスト3のビューバインディングの利用設定を行うと、先述のように、レイアウトxmlファイルの記述を元に、自動的にオブジェクトが作られます。このオブジェクトのクラス名は、レイアウトxmlファイル名をキャメル記法(パスカル記法)に変換した上でBindingをつけたものとなります。リスト4では、activity_main.xmlファイルを元にしているので、ActivityMainにBindingをつけ、ActivityMainBindingとなります。このオブジェクトを宣言したprivateフィールドが(1)であり、その取得、および、利用コードが(2)〜(4)です。こちらのコードに関しては後述するとして、このActivityMainBindingには、自動的にactivity_main.xmlに記述された画面部品がpublicフィールドとして定義されます(※)。そのため、データを表示するためのsetText()の実行も、(5)のようにそのフィールドに対して実行すれば問題なく反映されます。リスナの設定も、同様に、(6)のようにフィールドに対して設定すれば問題なく動作します。この仕組みにより、findViewById()を利用する必要がなくなり、id違いによるnullの問題や、データ型指定による型違いの問題も回避できます。(※)より正確には、idが設定された画面部品のみが定義されます。ConstraintLayoutを利用した画面作成においてはidの設定は必須なので、全ての画面部品がフィールドとして定義されているといっても問題ないといえます。バインディングオブジェクトの用意このように便利なバインディングクラス(リスト4ではActivityMainBindingクラス)は、再三説明してきているように、自動生成されています。利用するためには定型コードを記述する必要があります。それが、リスト4の(2)〜(4)です。バインディングオブジェクト本体を取得するコードが(2)であり、これは、該当バインディングクラス(ActivityMainBinding)に自動生成されたstaticメソッドinflate()を実行するだけです。その際、LayoutInflaterオブジェクトを渡す必要があります。これは、アクティビティのgetLayoutInflater()メソッドの戻り値を渡します。その後、生成されたバインディングオブジェクトのgetRoot()メソッドを実行します。このメソッドにより、レイアウトxmlファイルを元に生成(インフレート)された画面全体のインスタンスが返ってきます((3)ではcontentView)。このcontentViewを引数としてsetContentView()を実行します。アクティビティで表示画面を設定するsetContentView()の引数は、これまで、表示する画面のレイアウトxmlファイルのR値でした。その代わりに、バインディングオブジェクトによって生成された画面インスタンスを渡すことで、ビューバインディングが利用できるようになります。このバインディングオブジェクトを、(1)のようにフィールドとして保持することで、アクティビティ内ではいつでも画面部品にアクセスすることができるようになり、結果、findViewById()コードをアクティビティから一掃することができます。Kotlinコードでは、リスト5のようになり、リスト4を単にKotlinコードに置き換えたコードとなります。(6)}フラグメントでの利用(Java版)ここまでのコードは、アクティビティを参考に紹介してきました。このバインディングオブジェクトをフラグメントで利用する場合も、同様に考えることができます。フラグメントでは、画面を生成するライフサイクルコールバックメソッドがアクティビティとは違い、onCreateView()です。そこで、このメソッド内にリスト6の(1)〜(3)のようなバイディングオブジェクトの生成処理を記述します。(4)}リスト6のコードのポイントは、バインディングオブジェクトを取得する際のinflate()メソッドとして、引数が3個のものを利用し、第1引数と第2引数にはonCreateView()メソッドの引数をそのまま渡します。第3引数にはfalseを指定します。このようにして取得したバインディングオブジェクトから、レイアウトxmlファイルを元にインフレートされた画面全体のインスタンス(contentView)を取得するのは、アクティビティ同様に(2)のgetRoot()です。フラグメントのonCreateView()メソッドでは、(3)のように、このcontentViewを戻り値としてリターンします。このように取得したバインディングオブジェクトは、フラグメントの画面インスタンスの破棄と同時に破棄しておく必要があります。そのため、リスト6の(4)のように、onDestroyView()メソッドを実装し、フィールドで保持しているバインディングオブジェクトにnullを代入しておきます。フラグメントでの利用(Kotlin版)これらのフラグメントでのビューバインディングの使い方は、Kotlinでも同様で、リスト6をKotlinの置き換えたようなリスト7のコードになります。(4)}ただし、onDestroyView()でnullを代入しやすいように、また、フラグメント中でプロパティとして保持したバインディングオブジェクトを使いやすいようなコードとしておく必要があります。そのため、バインディングオブジェクト本体を、(1)のように別名で、かつ、nullableとしておきます。(2)のように、(1)のバインディングオブジェクト本体をnullでなくしたバンディングオブジェクトのゲッタを用意します。これらのコードのおかげで、フラグメント内では、(3)のように、nullではないことを前提にバインディングオブジェクトの各プロパティやメソッドを利用できるようになる一方で、(4)のようにonDestroyView()ではその本体にnullを代入できるようになります。