Java+JavaFX+MVVMでストップウォッチアプリを作成してみた所感


概要

 この2記事に触発され、私もJavaFXの練習がてら、ストップウォッチアプリを作成してみることにしました。もっとも、2つ目の記事は1つ目の記事に触発されて書かれたものですので、私の記事は2番煎じということになりますが……。

実装の方針

  • Java+JavaFXを利用してUI設計およびロジックを実装する
  • JavaFXはデフォルトではMVCパターンを意識したファイル名になっているが、あえてMVVMパターンによる開発を目指す
  • 普段はC#+WPF+MVVM+ReactivePropertyで開発しているので、言語仕様の違いに気をつけながら開発する

開発ステップ

ファイル名の変更

 Javaでは「1ファイル1クラス」といった原則がありますので、*.javaのファイル名を決めることはクラス名を決めることに直結します。前述したようにMVVMパターンで実装するので、

  • Sample.fxmlからMainView.fxml
  • Controller.javaからMainViewModel.java
  • MainModel.javaも新規作成

するようにしました。

UI設計

 C#+WPFではVisual Studio上でXAMLデザイナーが使えますので、そこで設計すればよいだけの話でした。それだけに、Xamarin.Formsでプレビューがろくすっぽ効かなかった時はなかなかに苦痛でしたが……。

 一方、Java+JavaFXの場合、JavaFX Scene BuilderでUI設計を行います(EclipseもIntelliJもこのツールに対応)。ここで編集して保存すると、IDEが当該FXMLファイルを再読み込みして反映されるわけですね(逆もまた然り)。

 XAMLもFXMLも、同じくXMLをベースに生み出された言語です。ただ、それぞれのコンテナ名やコントロール名は大きく異なります。一例を示しますと、

FXML XAML 意味
AnchorPane 対応なし コンテナいっぱいまで中のコントロールを広げる
BorderPane (DockPanelで代用可) 上・左・中央・右・下のどれかにコントロールを配置
FlowPane WrapPanel コントロール配置を折り返す機能が付いたコンテナ
GridPane Grid 格子で区切った場所にコントロールを配置
HBox,VBox StackPanel 縦や横にコントロールを並べて配置
TabPane TabControl タブ表示の中にコントロールを配置
ToolBar ToolBarPanel ツールバーを表示する
DatePicker Calendar カレンダーから日付を表示・選択する
Hyperlink Link リンク付きテキストを表示する
ImageView Image 画像を表示する
MenuBar Menu メニューを配置
Menu MenuItem メニューの各項目(クリックすると子メニューを展開)
MenuItem MenuItem メニューの各項目(クリックすると別のアクションを起こす)
TextArea TextBox 複数行テキスト入力(WPFではプロパティ設定で対応)
TextField TextBox 1行テキスト入力
WebView WebBrowser Webブラウザだが、FXMLだとWebKitなのでより上等

といった具合です。また、コントロールやコンテナのサイズ指定の意味合いがだいぶ異なっており、XAML脳だとなかなかサイズが思い通りに決まらないといったトラブルを招きます。詳しくは、次のWebページを参考にしてください。

 ちなみに、Scene Builderには、コントロールをポン付けするとなんでもかんでもGridとMarginで配置してしまうクソ仕様なんてありませんので、その辺はご安心くださいw

Modelの設計、およびViewModelによるViewとの接続

 前述したように、FXMLはMVVMを意識した設計にはなっていません。そのせいか、デフォルトではコントロールに名前を付けてコードビハインドで直接アクセスしたり、Controllerのメソッドでボタンを押した際のロジックを直接記述するなどといった、WinFormsを彷彿とさせるコーディングになってしまいます。

 ただ、都合がいいことに、JavaFXには標準で「StringPropertyなどの通知機構付きの型」と「ChangeListenerなどの通知機能」と「bindBidirectionalなどのData Binding機能」が搭載されています。これらを生かすと、

  • ModelにStringPropertyなどのプロパティを配置
  • ViewModelでコントロールのインスタンス(つまりView側)を定義し、bindBidirectionalsetOnActionなどでModelと紐付け(Data Binding)
  • プロパティ同士はChangeListenerなどを使えば変更を通知して連動できる

といった素敵環境が完成します。より楽したいならRxJavaを使うべきなのだろうとは思いましたが、Data Bindingだけなら別に外部ライブラリなしで全然書けるなと思いました。

 ちなみにJavaFXにおけるプロパティは、C#におけるプロパティと意味合いが全然違うのでご注意ください。また、Javaの本元(Oracle)は「高レベルバインディングAPI」と「低レベルバインディングAPI」を使おうと勧めてくるのですが、下のコード例を見れば分かるように、ChangeListener一本で書いた方が楽だと思います。

sample.java
// 本家が勧めてくる「高レベル・バインディングAPI」とやら
IntegerProperty num1 = new SimpleIntegerProperty(1);
IntegerProperty num2 = new SimpleIntegerProperty(2);
IntegerProperty num3 = new SimpleIntegerProperty(3);
IntegerProperty num4 = new SimpleIntegerProperty(4);
NumberBinding total =
    Bindings.add(num1.multiply(num2),num3.multiply(num4));
System.out.println(total.getValue());

// こう書いた方が可読性が高いのでは??
IntegerProperty num1 = new SimpleIntegerProperty(1);
IntegerProperty num2 = new SimpleIntegerProperty(2);
IntegerProperty num3 = new SimpleIntegerProperty(3);
IntegerProperty num4 = new SimpleIntegerProperty(4);
    int total;
    Runnable runner = () -> {
        total = num1.get() * num2.get() + num3.get() * num4.get();
    };
num1.addListener((ob, oldVal, newVal) -> setTotal());
num2.addListener((ob, oldVal, newVal) -> setTotal());
num3.addListener((ob, oldVal, newVal) -> setTotal());
num4.addListener((ob, oldVal, newVal) -> setTotal());
    System.out.println(total);

ロジックの実装

 ここまで来ると、JavaFXみが薄れてきますね(Modelだけ考えればいいのがMVVMの強み)。先人の反省点に倣い、次の方針でロジックを実装しました。

  • タイマーが起動していない状態でスタート/ストップボタンを押すと、押した瞬間の日時(A)を記録する。また、タイマーが起動し、ラップボタンが押せるようになる。この際、ラップ開始の日時Bの初期値をAと定義する(もしくは、Aを取得した直後にBも取得する)
  • ラップボタンを押すと、日時Bと押した瞬間の日時(C)からラップタイムを計算し、ラップタイム一覧(D)にラップタイムを追加する。また、リストビューにそのタイムを追記する
  • タイマーが起動した状態でスタート/ストップボタンを押すと、ラップタイム追記の他、一覧Dから最速ラップタイムと最遅ラップタイムを算出してダイアログ表示する
  • タイマーはUIスレッドとは違うスレッドで動作しており、定期的(10ms間隔など)に計測タイムの更新を行う。ただ、UIスレッド以外が、UIやそれに関連したプロパティ(Data Bindingで紐付けされたものも含む)を弄ると実行時エラーになるので、Platform.runLaterメソッドで更新処理をUIスレッドに移譲するのが大事

 参考:JavaFXでスレッドを使って描画するときの注意 - Qiita

まとめ

 Java+JavaFXで、MVVMおよびData Bindingを生かしたアプリ開発をすることができました。ReactivePropertyのようなお助け補助ライブラリがない状況だったので少し面倒でしたが、予想より楽に書き上げることができて正直驚いています。JavaFXの、Scene Builderとプロパティ機能が持つポテンシャルは侮れませんね……!

 また、Javaで開発する際に忘れてはいけないのがラムダ式とStream API。前者はC#と同じような書き心地で、後者もLINQほど便利ではないにせよ役立ちました。

 ところで、JavaFXのプロパティ機能は、ReactivePropertyと違ってStringPropertyだのIntegerPropertyだのと変数型が決め打ちなものばかりで、Property<T>型がなぜかinterfaceなのは何故なのでしょう……?

参考資料