How to construct the builder pattern around JavaFX componenets in Kotlin?


#1

I am trying to come up with a builder pattern in Kotlin for javafx components. The pattern will look like below

fun main(args: Array<String>) {
    val vb = vbox {
        child {
            hbox {
                child {
                    label {
                        prefWidth = 20.0
                    }
                    button {
                        text = "Click"
                    }
                }
            }
            label {
                prefHeight = 80.0
            }
        }
    }
}

What I have done so far is as below, but the child is not exposing the label or the button or any of the methods declared in Child class. What am I missing here?

fun Pane.child(init: (Pane.() -> Unit)? = null): Child {
    val ch = Child()
    init?.invoke(this)
    ch.parent = this
    return ch
}

class Child {
    var parent: Pane? = null

    fun <T : Node> initChildNode(styleClass: String? = null, tag: T, init: (T.() -> Unit)? = null): T {
        init?.invoke(tag)
        tag.styleClass.add(styleClass)
        parent?.children?.add(tag)
        return tag
    }

    fun region(styleClass: String? = null, init: (Region.() -> Unit)? = null) = initChildNode(styleClass, Region(), init)
    fun vbox(styleClass: String? = null, init: (VBox.() -> Unit)? = null) = initChildNode(styleClass, VBox(), init)
    fun hbox(styleClass: String? = null, init: (HBox.() -> Unit)? = null) = initChildNode(styleClass, HBox(), init)
    fun label(styleClass: String? = null, init: (Label.() -> Unit)? = null) = initChildNode(styleClass, Label(), init)
    fun button(styleClass: String? = null, init: (Button.() -> Unit)? = null) = initChildNode(styleClass, Button(), init)

}

fun vbox(styleClass: String? = null, init: (VBox.() -> Unit)? = null) = initNode(styleClass, VBox(), init)
fun hbox(styleClass: String? = null, init: (HBox.() -> Unit)? = null) = initNode(styleClass, HBox(), init)

fun <T : Node> initNode(styleClass: String? = null, tag: T, init: (T.() -> Unit)? = null): T {
    init?.invoke(tag)
    tag.styleClass.add(styleClass)
    return tag
}

I know about the TornadoFX library and its builder interface, but I like to come up with a solution of my own mostly due to learning purpose.


#2

You seem to be missing the fact that child has to have Child as the receiver of its init lambda (not the Pane!). I also hightly recommend to take a look at Hadi’s presentation on building DSLs with Kotlin to better understand all the concepts: https://www.youtube.com/watch?v=GjGQpSFieXA


#3

Are you seen https://github.com/edvin/tornadofx ?


#4

@elizarov thanks for pointing it out. I have finally made it to work. Here it is for future reference:

fun <T : Pane> T.children(init: (Children.() -> Unit)? = null): Children {
    val ch = Children()
    ch.parent = this
    init?.invoke(ch)
    return ch
}

class Children : Layout() {
    var parent: Pane? = null

    override fun <T : Node> initNode(styleClass: String?, tag: T, init: (T.() -> Unit)?): T {
        val node: T = super.initNode(styleClass, tag, init)
        parent?.children?.add(node)
        return node
    }
}

open class Layout {
    var pane: Pane? = null

    open fun <T : Node> initNode(styleClass: String? = null, tag: T, init: (T.() -> Unit)? = null): T {
        init?.invoke(tag)
        tag.styleClass.add(styleClass)
        return tag
    }

    fun region(styleClass: String? = null, init: (Region.() -> Unit)? = null) = initNode(styleClass, Region(), init)
    fun vbox(styleClass: String? = null, init: (VBox.() -> Unit)? = null) = initNode(styleClass, VBox(), init)
    fun hbox(styleClass: String? = null, init: (HBox.() -> Unit)? = null) = initNode(styleClass, HBox(), init)
    fun label(styleClass: String? = null, init: (Label.() -> Unit)? = null) = initNode(styleClass, Label(), init)
    fun button(styleClass: String? = null, init: (Button.() -> Unit)? = null) = initNode(styleClass, Button(), init)
    fun progressBar(styleClass: String? = null, init: (ProgressBar.() -> Unit)? = null) = initNode(styleClass, ProgressBar(), init)
}

fun <T : Pane> layout(styleSheet: String = "", init: Layout.() -> T): T {
    val layout = Layout()
    val pane = layout.init()
    layout.pane = pane
    pane.stylesheets.add(layout.javaClass.classLoader.getResource(styleSheet).toExternalForm())
    return pane
}

And the usage is like below

private var loadProgress: ProgressBar? = null
private var progressText: Label? = null

val layout = layout("xyz.css") {
            vbox("screen") {
                prefWidth = 500
                prefHeight = 450
                children {
                    region { minHeight = 250.0 }
                    hbox {
                        children {
                            region { minWidth = 30.0 }
                            label("large-label") {
                                text = "Foo"
                            }
                            label("large-label") {
                                text = "Bar"
                            }
                            region { minWidth = 20.0 }
                            label("small-label") {
                                text = "xyz"
                            }
                        }
                    }
                    region { minHeight = 10.0 }
                    loadProgress = progressBar("progress") { minWidth = this@vbox.prefWidth }
                    hbox {
                        children {
                            region { minWidth = 30.0 }
                            progressText = label("progress-label")
                        }
                    }
                }
            }
        }

#5

Yes I have already seen that. I just wanted to come out with my own solution mainly for learning things.