データバインディングとエンティティ
前節の最後に式言語を利用したコードの問題点を指摘しました。その解決策を紹介する前に、それにつながるデータバインディングの仕組みを先に紹介していきます。
データ項目が増えたサンプル
前節までのサンプルでは、データバインディングとして利用するデータ項目は、randNumという乱数ひとつのみでした。このように画面に表示するデータがひとつだけなのは稀であり、複数のデータ項目を扱うのが通常です。そこで、データ項目を増やしたサンプルを用意しました。その画面は、図1のようなものです。ボタンをタップすると、0から10の間の乱数を2個発生させ、それを四角形の縦の長さと横の長さとします。それらの値をもとに面積を計算し、表示させるものです。これだけで画面に関与するデータ項目が3個に増え、さらに、ボタンも含めると、関与する画面部品が4個となります。
この画面の表示データとして縦の長さのheight、横の長さのwidth、面積のareaとすると、データバインディングの設定をレイアウトxmlに記述するにあたって、variableタグを、height、width、areaの3個定義しても問題なく動作します。しかし、当然ですがデータ項目が増えると、それだけコードが煩瑣になるだけでなく、後述するように不便なことも起こります。
エンティティオブジェクトの利用
そこで、画面表示に必要なデータ項目をひとつのオブジェクトとし、まとめてデータを反映させます。例えば、今回のサンプルでは、データをまとめておくクラスとして、Rectangleのようなものを用意するとします。このRectangleのようなオブジェクトを、データオブジェクトと言ったり、エンティティと言ったりします(本稿ではエンティティと記載していきます)。Rectangleエンティティクラスの構造に関しては後述するとして、このRectangleを使ってデータバインディングを設定すると、レイアウトxmlはリスト1のような内容となります。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" …> <data> <variable name="rectangle" type="com.….Rectangle" /> (1) </data> <androidx.constraintlayout.widget.ConstraintLayout …> : <TextView android:id="@+id/tvHeight" : android:text="@{rectangle.heightStr}"/> (2) : <TextView android:id="@+id/tvWidth" : android:text="@{rectangle.widthStr}"/> (3) : <TextView android:id="@+id/tvArea" : android:text="@{rectangle.areaStr}"/> (4) : </androidx.constraintlayout.widget.ConstraintLayout> </layout>
リスト1のポイントは、(1)です。variableタグのデータ型を指定するtype属性に、エンティティクラスの完全修飾名を指定します。これだけで、name属性で指定した変数名に対して.(ドット)でアクセスするだけで、エンティティオブジェクト内に格納されたデータをレイアウト記述内で利用できるようになります。例えば、リスト1の(2)ではrectangle.heightStrという記述で、RectangleエンティティオブジェクトであるrectangleのheightStrプロパティにアクセスし、そのデータを表示します。結果、縦の長さが表示されます。横の長さの(3)、面積の(4)も同様です。
エンティティクラスの作り方(Java版)
ところで、リスト1では、(2)の縦の長さを表すプロパティが、heightではなくheightStrとなっています。(3)の横の長さ、(4)の面積も同様です。その種明かしも含めて、ここで、Rectangleのようなエンティティクラスの作り方を紹介します。Rectangleクラスは、Javaではリスト2のようなコードになります。
public class Rectangle { private final int _height; // (1) private final int _width; // (2) public Rectangle(int height, int width) { // (3) _height = height; _width = width; } public int getArea() { // (4) return _height * _width; } public String getAreaStr() { // (5) return String.valueOf(getArea()); } public int getHeight() { // (6) return _height; } public int getWidth() { // (7) return _width; } public String getHeightStr() { // (8) return String.valueOf(_height); } public String getWidthStr() { // (9) return String.valueOf(_width); } }
まず、エンティティクラスを作成する際の注意点は、イミュータブル(不変)オブジェクトとして作る必要があることです。そのため、縦の長さを表すリスト2の(1)と横の長さを表す(2)のように、フィールドはfinalキーワードをつけて、値を変更できないようにしておきます。
そして、(3)のようにコンストラクタでそれらのフィールドの値を受け取るようにしておきます。また、フィールドの値を変更するセッタも定義しません。このコードパターンのおかげで、エンティティクラスをnewした際に渡したデータが、その後変更できないようになります。
一方、フィールドの値を利用したゲッタはさまざま用意できます。リスト2では、単にフィールドの値をリターンするゲッタを、(6)と(7)に定義しています。また、面積に関しても、面積を保持するフィールドを用意するのではなく、(4)のようにその都度計算した値をリターンするゲッタを用意することで対応します。
データバインディングでは、これらのゲッタをレイアウトxmlで利用する場合、実は、以下のようなコードを記述する必要はありません。
@{rectangle.getArea()}
単に、プロパティアクセスのように、以下のコードを記述するだけです。
@{rectangle.area}
このことから、リスト1の(2)などのコード、すなわちheightStrプロパティなどへのアクセスのカラクリが理解できると思います。実は、このコードは、リスト2の(8)のgetHeightStr()ゲッタへのアクセスを意味します。
そして、前節で説明したように、text属性に対して@{ }を記述する場合は、Stringへの変換が必要なため、その変換処理を含んだゲッタを用意したのが、リスト2の(5)と(8)と(9)です。
このように、データをまとめて保持できるエンティティを用意し、そのデータの計算や加工、Stringへの変換処理も、ゲッタ(加工ゲッタ)の形でそのエンティティに含めてしまうことで、非常にスッキリしたコードとなります。前節の最後に指摘した、式言語に複雑なコードを記述せず、単にプロパティアクセスのコードで済ますための解決法も、この加工ゲッタです。
エンティティクラスの作り方(Kotlin版)
このエンティティクラスであるRectangleをKotlinでコーディングする場合は、リスト3のようになります。
data class Rectangle(val height: Int, val width: Int) { // (1) fun getArea(): Int { // (2) return height * width } fun getAreaStr(): String { // (3) return getArea().toString() } fun getHeightStr(): String { // (4) return height.toString() } fun getWidthStr(): String { // (5) return width.toString() } }
Kotlinの場合は、Javaと違い、dataクラスという便利な仕組みがあるので、それを利用します。リスト3の(1)のように、class宣言にdataキーワードがついているのは、そのためです。また、コンストラクタの引数をvalで定義することで、イミュータブルオブジェクトが実現できます。
このように、Kotlinではdataクラスを利用するため、クラス内に定義するメソッドは、Java版であるリスト2の(4)、(5)、(8)、(9)に該当する加工ゲッタのみとなります。リスト3の(2)がリスト2の(4)を、(3)が(5)を、(4)が(8)を、(5)が(9)をそのままKotlinコードに置き換えたコードとなっているのはそのためです。
アクティビティではエンティティをまとめてセット
このようなエンティティを用意すると、アクティビティ側でのコードは、エンティティをnewしてデータを格納し、まとめてバインディングオブジェクトにセットするものとなります。コードサンプル的には、Javaではリスト4、Kotlinではリスト5のようなコードになります。ただし、※のコードは、通常はViewModelなど、アクティビティ以外のクラスに記述します。
int height = (int) (Math.random() * 10) + 1; // ※ int width = (int) (Math.random() * 10) + 1; // ※ Rectangle rectangle = new Rectangle(height, width); // ※ _activityMainBinding.setRectangle(rectangle);
val height = (Math.random() * 10).toInt() + 1 // ※ val width = (Math.random() * 10).toInt() + 1 // ※ val rectangle = Rectangle(height, width) // ※ _activityMainBinding.rectangle = rectangle