JavaFXのFxmlの動的ロードをKotlinの力で簡易化するTips

# あらまし

`JavaFX`のビューテンプレートである`Fxml`を同的にロードする場合、

```kotlin
val widgetName = "/com/example/SomeWidget.fxml"
val uri = this.javaClass.getResource(widgetName)
val loader = FXMLLoader(uri)
val root = loader.load<Parent>()
val controller: SomeContainer = loader.getController()
```

という記述を毎回することなり、なかなかにメンドくさい。

なんとか、`Kotlin`の力で簡易化できないか模索した。

# 詳細

ボイラープレートな決まり切ったコードであるため、どこかにまとめて定義してやれば良い。
真っ先に思いつくのが親クラスに押し込むことであるが、この目的のために使うには仰々しいし、骨格抽象実装クラスとは言え、具象クラスを派生するのは嬉しくない。

なんとかならないかなと`kotlin`のリファレンスをながめていたところ、言語レベルで**移譲**をサポートしていると言うのを目にした。

https://dogwood008.github.io/kotlin-web-site-ja/docs/reference/delegation.html

具体的には、

```kotlin
class MyClass(delegate: DelegateImpl): DelegateIntf by delegate { 
// ... 
}
```

としてやれば良いようである。
このコンストラクタの引数に、`Fxml`動的コード生成を押し込んでやれば良さそうである。

## 追加要望

上述の動的ロードのコードでリソースパスを文字列で指定している。
これは精神衛生上よろしくない [^1]。

[^1]: フルパスにすべきところで先頭のスラッシュ(`/`)を書き忘れてロードされなかった恨みも含む

なんとか型の力で解決させたいため、以下の前提条件を設けた。
アーキテクチャとして、

* `Fxml`は`View`
* イベントをハンドリングするクラスは`Controller`

と定められているが、これを

* `Fxml`を`View`テンプレート
* イベントをハンドリングするクラスを`View`ハンドラ

と読み替え、二つ合わせて`View`として扱う。
同じものなのだから、クラス名と`Fxml`の`Uri`同一にできる(`.`と`/`の違いのみ)

したがって、

```kotlin
val widgetName = MyController::class.java.canonicalName.replace(".", "/")
val resourceName = "/${widgetName}.fxml"
```

のように、`View`ハンドラ (`Controller`)のクラス名から作成可能となる。

# 実装

以上の内容を基に作成したコードが以下に示す。

```kotlin
import javafx.fxml.FXMLLoader
import javafx.scene.Parent
import javafx.scene.Node

import java.lang.Class
import java.util.function.Consumer

public interface FxmlLoadHelper<R: Parent> {
    fun <T: Any> load(root: R, controller: T, cls: Class<T>): Unit 
    fun appendWith(consumer: Consumer<R>): Unit 
}

public class FxmlLoadHelperImpl<R: Parent>: FxmlLoadHelper<R> {
    private lateinit var _root: R

    override fun <T: Any> load(root: R, controller: T, cls: Class<T>): Unit {
        val widgetName = cls.canonicalName.replace(".", "/")

        val widgetFxml = this.javaClass.getResource("/${widgetName}.fxml")
        val widgetLoader = FXMLLoader(widgetFxml)

        widgetLoader.setController(controller)
        widgetLoader.setRoot(root)

        _root = widgetLoader.load<R>()
    }

    override fun appendWith(consumer: Consumer<R>): Unit {
        consumer.accept(this._root)
    }
}
```

最初に示した`Fxml`の動的ロードは、ロードの際に自動的に`Controller`をインスタンス化させているが、
`Kotlin`の移譲を使用するため、公式ドキュメントのカスタムコンポーネントの方法([FXMLの概要 -  FXMLLoader > カスタム・コンポーネント](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/fxml/doc-files/introduction_to_fxml.html#custom_components))を採用していることに注意。

`appendWith`は、呼び出し元に関与する`Scene`とシーングラフを構成するためのメソッド。
呼び出し元への割り当てが、`VBox`のような子コントロールへの追加だったり、`BorderPane`へのセットだったりと、一概に決めれないため、呼び出し側でよしなにできるよう`Consumer`型を受け付けるようにしている。

## 使用例

コントローラの定義は以下の通り。

```kotlin
import javafx.scene.Parent
import javafx.scene.layout.HBox

class MainMenuWidget(private val helper: FxmlLoadHelper<HBox> = FxmlLoadHelperImpl<HBox>()): FxmlLoadHelper<HBox> by helper {
    init {
        helper.load<MainMenuWidget>(HBox(), this, this.javaClass)
    }
}
```

コントローラごとに差異の出るものでもないため、移譲する具象クラスをデフォルト引数として与えている。
そのため、コントローラをインスタンス化する際に引数に気を向ける必要がなくなる。

`init`ブロックでヘルパの`load`メソッドを呼ぶ。
`load`メソッドには、展開先のルートノードのインスタンス、コントローラークラスのインスタンス(`this`)、コントローラクラスの型を渡す。

次にコントローラを呼び出す側。

```kotlin
val menuBar = MainMenuWidget()
menuBar.appendWith { child -> this.root.setTop(child) }
```

ここで呼び出し側の`Scene`オブジェクトは`BorderPane`。

`VBox`のような子コレクションに追加するのであれば

```kotlin
menuBar.appendWith { child -> this.root.children.add(child) }
```

となるだろう。

## 補足事項

`FxmlLoadHelper`インターフェースをジェネリックとしている。
これは、あとでルートノードの具体的な方が必要になるのかどうか、不明だったため念のためつけている。

ここに提示した例程度であればジェネリックインターフェースにする必要もなく

* `_root`プロパティの肩は`Parent`
* `Consumer`のジェネリック引数は`Node`

としても問題ない。