画面部品の取得を自動化できるビューバインディング
今回のテーマは、画面へのデータ反映処理を自動化できるデータバインディングです。そのデータバインディングを紹介する前に、画面へデータを反映させる従来の方法から話を始め、ビューバインディングを紹介します。
なお、今回のサンプルデータは、GitHubから参照できます。
画面部品の従来の扱い方
例えば、図1のようなシンプルなアプリを考えてみます。このアプリは、ボタンをタップすると、乱数を発生させて、それを表示させるものです。
乱数の発生や発生した乱数値の保持を第1回で紹介したViewModelに任せるとしても、そのデータを表示させるのはアクティビティの役割であり、従来は、Javaではリスト1の、Kotlinではリスト2の(1)と(2)のコードを記述していました。また、ボタンのリスナ登録も、(3)と(4)のようなコードを記述していました。
TextView tvRand = findViewById(R.id.tvRand); // (1) tvRand.setText(_mainViewModel.getRandNumStr()); // (2) Button btnRand = findViewById(R.id.btnRand); // (3) btnRand.setOnClickListener(new ButtonClickListener()); // (4)
val tvRand = findViewById<TextView>(R.id.tvRand) // (1) tvRand.text = _mainViewModel.getRandNumStr() // (2) val btnRand = findViewById<Button>(R.id.btnRand) // (3) btnRand.setOnClickListener(ButtonClickListener()) // (4)
ここで注目したいのは、(1)と(3)です。画面部品に対して、データを表示したりリスナを設定したりなど、何か処理を行う場合は、まず、画面部品のidのR値を引数として、findViewById()メソッドで該当画面部品を取得する必要があります。
このfindViewById()を利用したコードは、画面部品を利用するたびに記述する必要があり、そのぶん、アクティビティ内のコードが膨らむ傾向があります。さらに、コード量の問題だけではなく、nullとデータ型の問題を孕んでいます。まず、idのR値として、現在表示させている画面とは別のレイアウトxmlに記述されたidを指定すると、findViewById()の戻り値はnullとなり、結果的にその後のコード(例えば、(2)や(4))でNullPointerExceptionが発生します。また、戻り値を格納する変数のデータ型(Kotlinコードならばジェネリクスで指定するデータ型)として違うものを記述すると、ClassCastExceptioinが発生します。つまり、findViewById()はnull安全でもなく、型安全でもない、ということです。
ビューバインディングとその利用設定
このような問題を解決するために、Jetpackではビューバインディングという仕組みが導入されています。このビューバインディングでは、レイアウトxmlファイルの記述を元に、その画面に含まれる画面部品が格納されたオブジェクトを自動生成させる仕組みです。
このビューバインディングを利用する場合、まず、プロジェクトに設定が必要です。build.gradle.kts(Module: app)に、リスト3の太字の追記を行います。ファイル直下に既に存在しているandroidブロックにbuildFeaturesブロックを追加し、viewBindingプロパティをtrueとします。
plugins { : } android { : buildFeatures { viewBinding = true } } dependencies { : }
ビューバインディングの利用コード
これで、プロジェクト内でビューバインディングが利用できるようになります。そして、そのビューバインディングを利用すると、リスト1の画面部品を扱うコードは、Javaではリスト4の(5)や(6)のようになります。
private ActivityMainBinding _activityMainBinding; // (1) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); _activityMainBinding = ActivityMainBinding.inflate(getLayoutInflater()); // (2) View contentView = _activityMainBinding.getRoot(); // (3) setContentView(contentView); // (4) : _activityMainBinding.tvRand.setText(_mainViewModel.getRandNumStr()); // (5) _activityMainBinding.btnRand.setOnClickListener(new ButtonClickListener()); // (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()メソッドを実行します。それが(3)です。このメソッドにより、レイアウトxmlファイルを元に生成(インフレート)された画面全体のインスタンスが返ってきます((3)ではcontentView)。そして、このcontentViewを引数としてsetContentView()を実行します。それが(4)です。
アクティビティで表示画面を設定するsetContentView()の引数は、これまで、表示する画面のレイアウトxmlファイルのR値でした。その代わりに、バインディングオブジェクトによって生成された画面インスタンスを渡すことで、ビューバインディングが利用できるようになります。
そして、このバインディングオブジェクトを、(1)のようにフィールドとして保持することで、アクティビティ内ではいつでも画面部品にアクセスすることができるようになり、結果、findViewById()コードをアクティビティから一掃することができます。
なお、Kotlinコードでは、リスト5のようになり、リスト4を単にKotlinコードに置き換えたコードとなります。
private lateinit var _activityMainBinding: ActivityMainBinding // (1) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _activityMainBinding = ActivityMainBinding.inflate(layoutInflater) // (2) setContentView(_activityMainBinding.root) // (3) & (4) : _activityMainBinding.tvRand.text = _mainViewModel.getRandNumStr() // (5) _activityMainBinding.btnRand.setOnClickListener(ButtonClickListener()) // (6) }
フラグメントでの利用(Java版)
ここまでのコードは、アクティビティを参考に紹介してきました。このバインディングオブジェクトをフラグメントで利用する場合も、同様に考えることができます。ただし、フラグメントでは、画面を生成するライフサイクルコールバックメソッドがアクティビティとは違い、onCreateView()です。そこで、このメソッド内にリスト6の(1)〜(3)のようなバイディングオブジェクトの生成処理を記述します。
private FragmentMainBinding _fragmentMainBinding; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { _fragmentMainBinding = FragmentMainBinding.inflate(inflater, container, false); // (1) View contentView = _fragmentMainBinding.getRoot(); // (2) return contentView; // (3) } @Override public void onDestroyView() { super.onDestroyView(); _fragmentMainBinding = null; // (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のコードになります。
private var _fragmentMainBindingInit: FragmentMainBinding? = null // (1) private val _fragmentMainBinding get() = _fragmentMainBindingInit!! // (2) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { _fragmentMainBindingInit = FragmentMainBinding.inflate(inflater, container, false) return _fragmentMainBinding.root // (3) } override fun onDestroyView() { super.onDestroyView() _fragmentMainBindingInit = null; // (4) }
ただし、onDestroyView()でnullを代入しやすいように、また、フラグメント中でプロパティとして保持したバインディングオブジェクトを使いやすいようなコードとしておく必要があります。そのため、バインディングオブジェクト本体を、(1)のように別名で、かつ、nullableとしておきます。
さらに、(2)のように、(1)のバインディングオブジェクト本体をnullでなくしたバンディングオブジェクトのゲッタを用意します。これらのコードのおかげで、フラグメント内では、(3)のように、nullではないことを前提にバインディングオブジェクトの各プロパティやメソッドを利用できるようになる一方で、(4)のようにonDestroyView()ではその本体にnullを代入できるようになります。