Search code examples
kotlinjavafxtornadofx

Why are variables declared after root automatically added to UI in TornadoFx?


I have stumbled upon a behaviour in TornadoFx that doesn't seem to be mentioned anywhere (I have searched a lot) and that I'm wondering about.

If I define a view like this with the TornadoFx builders for the labels:

class ExampleView: View() {

    override
    val root = vbox{ label("first label") }

    val secondLabel = label("second label")
}

The result is:

enter image description here

That is, the mere definition of secondLabel automatically adds it to the rootof the scene.

However, if I place this definition BEFORE the definition of root...

class ExampleView: View() {

    val secondLabel = Label("second label")

    override
    val root = vbox{ label("first label") }
}

... or if I use the JavaFx Labelclass instead of the TornadoFx builder ...

class ExampleView: View() {

    override
    val root = vbox{ label("first label") }

    val secondLabel = Label("second label")
}

... then it works as I expect:

enter image description here

Of course, I can simply define all variables in the view before I define the rootelement but I'm still curious why this behaviour exists; perhaps I am missing some general design rule or setting.


Solution

  • The builders in TornadoFX automatically attach themselves to the current parent in the scope they are called in. Therefore, if you call a builder function on the View itself, the generated ui component is automatically added to the root of that View. That's what you're seeing.

    If you really have a valid use case for creating a ui component outside of the hierarchy it should be housed in, you shouldn't call a builder function, but instead instantiate the element with it's constructor, like you did with Label(). However, the use cases for such behavior are slim to none.

    Best practice is to store value properties in the view or a view model and bind the property to the ui element using the builders. You then manipulate the value property when needed, and the change will automatically update in the ui. Therefore, you very very seldom have a need to access a specific ui element at a later stage. Example:

    val myProperty = SimpleStringProperty("Hello world")
    
    override val root = hbox {
        label(myProperty)
    }
    

    When you want to change the label value, you just update the property. (The property should be in an injected view model in a real world application).

    If you really need to have a reference to the ui element, you should declare the ui property first, then assign to it when you actually build the ui element. Define the ui property using the singleAssign() delegate to make sure you only assign to it once.

    var myLabel: Label by singleAssign()
    
    override val root = hbox {
        label("My label) {
            myLabel = this
        }
    }
    

    I want to stress again that this is very rarely needed, and if you feel you need it you should look to restructure your ui code to be more data driven.

    Another technique to avoid storing references to ui elements is to leverage the EventBus to listen for events. There are plenty of examples of this out there.