監視オブジェクトの利用
ここから、このgenerateNewHeight()、および、generateNewWidth()の処理コードに話を移していきます。
個別データの変更を監視するオブジェクト
その際、問題となるのが、再表示です。例えば、縦の長さが再生成された場合、当然ですが、縦の長さの表示欄と面積の表示欄の値を新しい値に変更する必要があります。一方で、横の長さの表示はそのままでも問題ありません。
確かに、前回導入したイミュータブルなエンティティを新たに生成し、再生成された値もあらかじめ存在する値も、すべての値を設定したものをバインドし直せば、表示も変更されます。
一方で、今回のサンプルのように、個別の値の変更に合わせて適切に表示を変える場合、無駄な処理が増え、使いづらいものとなります。可能ならば、エンティティ内の縦の長さを変更するだけで、面積表示も自動で変更できるのが望ましいでしょう。このように、個別のデータの変更を監視できるようにしたもの、つまり、監視オブジェクトを、エンティティとして利用することもできます。
前回のリスト2で作成したRectangleエンティティを、監視オブジェクトとする場合、リスト2のようになります。
public class Rectangle extends BaseObservable { // (1) private int _height = 0; // (2) private int _width = 0; // (3) public int getArea() { return _height * _width; } @Bindable // (4) public String getAreaStr() { return String.valueOf(getArea()); } @Bindable // (5) public String getHeightStr() { return String.valueOf(_height); } @Bindable // (6) public String getWidthStr() { return String.valueOf(_width); } public void setHeight(int height) { _height = height; notifyPropertyChanged(BR.heightStr); // (7) notifyPropertyChanged(BR.areaStr); // (8) } public void setWidth(int width) { _width = width; notifyPropertyChanged(BR.widthStr); // (9) notifyPropertyChanged(BR.areaStr); // (10) } }
監視オブジェクトを作成する場合のポイントは、リスト2の(1)のように、BaseObservableクラスを継承する点です。そして、データを表すフィールドの値を個別に変更できるように、(2)と(3)のようにfinalキーワードをつけず、ミュータブルなオブジェクトとします。
さらに、画面にバインドする値のゲッタに、@Bindableアノテーションを記述します。リスト2では、areaStrプロパティに該当する(4)のgetAreaStr()メソッド、heightStrに該当する(5)のgetHeightStr()メソッド、widthStrに該当する(6)のgetWidthStr()メソッドに@Bindableアノテーションが記述されています。そして、監視オブジェクトの監視を実現するコードが(7)と(8)、および、(9)と(10)です。
例えば、縦の長さの新しい値をRectangleに格納するには、heightのセッタであるsetHeight()を利用します。そのタイミングで、画面表示に利用されるheightStrプロパティ、および、areaStrプロパティの値を更新する必要があります。その更新処理を行うメソッドが、notifyPropertyChanged()であり、その引数に更新対象プロパティをBRクラスの定数として指定します。
このBRクラスは、データバインディングライブラリによって自動生成されたクラスであり、バインディング対象のプロパティを定数として保持しています。リスト2の(7)ではheightStrプロパティが、(8)ではareaStrプロパティが定数として指定されており、結果的にこの値がバインドされた画面表示が更新されます。
(9)と(10)も同様で、横の長さのセッタであるsetWidth()が呼び出されたタイミングで、widthStrとareaStrが更新されるようにしています。
Kotlin版監視オブジェクトの作り方
監視オブジェクトをKotlinで作成する場合も、リスト2をKotlinコードに置き換えたようなリスト3のコードとなります。ただし、Javaでは手動で定義したセッタとゲッタが、dataクラスを利用する場合は自動生成となっています。そのため、(1)や(4)のように、自動生成されたセッタをカスタマイズします。その中で、(2)や(3)、および、(5)や(6)のように、notifyPropertyChanged()を実行します。
class Rectangle: BaseObservable() { var height: Int = 0 set(value) { // (1) field = value notifyPropertyChanged(BR.heightStr) // (2) notifyPropertyChanged(BR.areaStr) // (3) } var width: Int = 0 set(value) { // (4) field = value notifyPropertyChanged(BR.widthStr) // (5) notifyPropertyChanged(BR.areaStr) // (6) } fun getArea(): Int { return height * width } @Bindable fun getAreaStr(): String { return getArea().toString() } @Bindable fun getHeightStr(): String { return height.toString() } @Bindable fun getWidthStr(): String { return width.toString() } }
ViewModel内ではセッタを実行
このような監視オブジェクトを作成しておき、MainViewModelのフィールドとしておきます。リスト1にもあるように、フィールドのRectangleオブジェクト内のデータをTextViewにバインドしているならば、縦の長さ、および、横の長さを再生成するメソッドであるgenerateNewHeight()、および、generateNewWidth()内のコードは、Javaではリスト4、Kotlinではリスト5のようになります。単に、新しい乱数を発生させて、セッタにセットするだけです。
public class MainViewModel extends ViewModel { private Rectangle _rectangle = new Rectangle(); : public void generateNewHeight() { int height = (int) (Math.random() * 10) + 1; _rectangle.setHeight(height); } public void generateNewWidth() { int width = (int) (Math.random() * 10) + 1; _rectangle.setWidth(width); } : }
class MainViewModel : ViewModel() { var rectangle: Rectangle = Rectangle() : fun generateNewHeight() { rectangle.height = (Math.random() * 10).toInt() + 1 } fun generateNewWidth() { rectangle.width = (Math.random() * 10).toInt() + 1 } }