Codetown ::: a software developer's community
Last week, we started our neighborhood cat scheduler application. I will be switching this series to occur on Thursdays, given the craziness in my personal life! Don’t forget you can view the full version of this project on my github!
This week, we wrap up our mini-application with a couple more cool TornadoFX features that make native development really fun!
Keep it DRY, right? Good coding conventions indicate that if you don’t have to repeat yourself, don’t. One thing I noticed right away is that my setup for tabbed tableviews is incredibly repetitive.
class BottomView: View( ) {
private val controller: BottomViewController by inject( )
private val model: CatScheduleModel by inject( )
override val root = hbox {
tabpane {
tab("Monday") {
tableview(controller.mondays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
tab("Tuesday") {
tableview(controller.tuesdays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
tab("Wednesday") {
tableview(controller.wednesdays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
tab("Thursday") {
tableview(controller.thursdays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
tab("Friday") {
tableview(controller.fridays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
addClass(Styles.schedule)
}
stackpane {
rectangle {
width = 200.0
height = 200.0
fill = Color.TRANSPARENT
}
imageView(cat, true)
}
}
}
Refactoring is a skill that improves over time, so always ask your coding buddies to review your code! Like in Java, you can use forEach in Kotlin:
class BottomView: View( ) {
private val controller: BottomViewController by inject( )
private val model: CatScheduleModel by inject( )
private var weekdays= listOf(
Pair("Monday", controller.mondays),
Pair("Tuesday", controller.tuesdays),
Pair("Wednesday", controller.wednesdays),
Pair("Thursday", controller.thursdays),
Pair("Friday", controller.fridays)
)
override val root = hbox {
tabpane {
weekdays.forEach {
tab(it.first) {
tableview(it.second) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
}
addClass(Styles.schedule)
}
stackpane {
rectangle {
width = 200.0
height = 200.0
fill = Color.TRANSPARENT
}
imageView(cat, true)
}
}
}
That’s a lot cleaner. Let’s continue on with some of the cool features we might want in our application.
Occasionally, life gets in the way of life; you definitely want the option to be able to change your designated times you might want to visit a client’s house or even correct the names you originally assigned for owners/cats.
One of the fresh features TornadoFX offers is the way you can leverage your data with tableviews — advanced data controls such as smartResize() and remainingWidth(), designed to give pleasant spacing for the data necessary in your columns. More importantly, databinding models/listening for changes for these tables have never been easier with TornadoFX. In the last post, we used Kotlin data classes to display our data; we will now edit our model with ViewModel, TornadoFX’s tool that helps cleanly separate your UI and business logic that allows for features like dirty-state checking and commits/rollback. With ViewModel, you can avoid manual rebinding of your data as it changes and tight-coupling (having to extract the object data again just to be able to reflect changes). You no longer have to bind, unbind, or rebind on change for a simple action such as editing your values. Whew!
Another perk of ViewModel is not having to worry about changing your data as you insert that data in ObservableValue fields until you call model.commit(). Once commit has been called, the data in the facade is flushed back into our person object and the table will now reflect our changes. Below, we use an extension of ViewModel called ItemViewModel that simply allows for easy getter/setter access via the item property.
class BottomView: View( ) {
private val controller: BottomViewController by inject( )
private val model: CatScheduleModel by inject( )
override val root = hbox {
tabpane {
tab("Monday") {
tableview(controller.mondays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
tab("Tuesday") {
tableview(controller.tuesdays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
tab("Wednesday") {
tableview(controller.wednesdays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
tab("Thursday") {
tableview(controller.thursdays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
tab("Friday") {
tableview(controller.fridays) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
}
}
addClass(Styles.schedule)
}
stackpane {
rectangle {
width = 200.0
height = 200.0
fill = Color.TRANSPARENT
}
imageView(cat, true)
}
}
}
Refactoring is a skill that improves over time, so always ask your coding buddies to review your code! Like in Java, you can use forEach in Kotlin:
class CatSchedule(ownerName: String, catName: : String, address: String,
time
: String, catImage : String) {val ownerNameProperty = SimpleStringProperty(this, "", ownerName
var ownerName by ownerNameProperty
val addressProperty = SimpleStringProperty(this, "", addressName
var catName by catNameProperty
val addressProperty = SimpleStringProperty(this, "", address
var address by addressProperty
val timeProperty = SimpleStringProperty(this, "", time
var time by timeProperty
val catImageProperty = SimpleStringProperty(this, "", catImage
var catImage by catImageProperty
}
class CatScheduleModel: StringItemViewModelspan style="color: #6f42c1;">CatSchedule>( ) {
val ownerName = bind(CatSchedule::ownerNameProperty)
val catName = bind(CatSchedule::catNameProperty)
val adress = bind(CatSchedule::addressProperty)
val time = bind(CatSchedule::timeProperty)
val catImage = bind(CatSchedule::catImageProperty)
}
All this data-binding can be leveraged with TornadoFX: let’s add a couple lines to our refactored tableview code. You may notice a new identifier I’m using in one of the global variables:
lateinit : this is a Kotlin concept that adheres to Kotlin’s null-safety feature. Non-null types must always be supplied with a constructor, if you can’t, but you still want to avoid null checks when referencing the property inside the body of a class.class BottomView: View( ) {
private val controller: BottomViewController by inject( )
private val model: CatScheduleModel by inject( )
lateinitvar avi: StackPane
private var weekdays= listOf(
Pair("Monday", controller.mondays),
Pair("Tuesday", controller.tuesdays),
Pair("Wednesday", controller.wednesdays),
Pair("Thursday", controller.thursdays),
Pair("Friday", controller.fridays)
)
override val root = hbox {
tabpane {
weekdays.forEach {
tab(it.first) {
tableview(it.second) {
column("Owner", CatSchedule::ownerName)
column("Cat", CatSchedule::catName)
column("Address", CatSchedule::address).remainingWidth( )
bindSelected(model)
smartResize( )
onUserSelect(1) {controller.changeCatAvi(it)}
onUserSelect(2) {controller.editCatSchedule(it)}
}
}
}
addClass(Styles.schedule)
}
avi = stackpane {
rectangle {
width = 200.0
height = 200.0
fill = Color.TRANSPARENT
}
imageView(cat, true)
}
}
}
Using this model, not only can we display our data, but we can call onUserSelect(# of clicks) to:
Very nice! Let’s talk about how we’re able to pass the table data to a new window.
According to TornadoFX, you get singleton instances when you use inject() or find() to locate a Controller or a View — meaning that wherever you locate that object in your code, you will get back the same instance. Scopes provide a way to make a View or Controller unique to a smaller subset of instances in your application. Scopes are simple constructs that can be used generally and once you understand its basic application, can be used for any limit you can stretch your mind to!
You define your own scope class to use the model property only available to this scope so that you may keep states.
class CatScheduleScope: Scope() {
val model = CatScheduleModel()
}With this scope instance, you may pass your selected data to the editor class.
fun editCatSchedule(catSchedule: CatSchedule) {
val catScheduleScope = CatScheduleScope( )
catScheduleScope.model.item = catSchedule
find(Editor::class, scope = catScheduleScope).openModal ( )
}
You’ll notice in the editor class already has an instance of the CatScheduleModel, so we configured the super.scope of the component Fragment when we open the editor. The editor class now holds information passed from the selection in the tableview, and can be displayed with ObservableValues in our textfield nodes. Last but not least, only allow the option to save when the model gets dirty and commit those changes!
class Editor: Fragment( ) {
// cast scope
override val scope = super.scope as CatScheduleScope
// extract our view model from the scope
var catNameField: TextField by SingleAssign( )
var timeField: TextField by SingleAssign( )
var ownerNameField: TextField by SingleAssign( )
override val root = hbox {
form {
fieldset("Edit person") {
field("Owner") {
textfield(model.ownerName)
ownerNameField = this
}
}
field("Cat") {
textfield(model.catName)
catNameField = this
}
}
field("Time") {
textfield(model.time)
timeField = this
}
}
button("Save") {
enableWhen(model.dirty)
action {
save( )
}
}
}
}
}
private fun save( ) {
// flush changes from the text field into the model
model.commit( )
// edited at schedule is contained in the model
val catSchedule = model.item
println("Saving Changes: ${catSchedule.ownerName} / ${catSchedule.catName} / ${catSchedule.time}
close( )
}
}
You’ll notice the use of Fragment for this particular component — any View you create is a singleton, meaning only one can exist at a time in the parent view. Fragments, on the other hand, is used for multiple instances, which is especially useful for nested popups or working parts of a larger UI.
As much as I wanted to add animation to this tutorial, I’m afraid that will go well beyond the scope of TornadoFX and the introductory aspects of JavaFX — but for practicality’s sake, let’s quickly gloss over how stackpane can be used to add anything you wish as an overlay to your views.
I edited these images as a single bubble icon, but what I describe is extremely similar to a concept discovered in a previous TornadoFX project for drag-and-drop GUI creation (you can also look into this project for my own version of scope use!). The concept is really simple! Create a stackpane as the main portion of the view, set isMouseTransparent = true so that you can click through the upper overlay, and set the opacity of the stackpane with any color with the opacity closest to 0 as possible.
class NeighborhoodView: View( ) {
private val controller: NeighborhoodController by inject( )
// set up neighborhood
override val root = stackpane {
gridpane {
// add your own gridpane constaints to set a certain margin if you wish
row {
imageview("speech_bubble4.png")
imageview("speech_bubble5.png")
}
row {
imageview("speech_bubble1.png")
imageview("speech_bubble2.png")
imageview("speech_bubble3.png")
}
isMouseTransparent = true
}
style { backgroundColor += c(0, 100, 100, 0.05 }
}
}
This isn’t TornadoFX related, but this is a concept I find incredibly useful in creating native applications.
I may have to switch to a different name for Kotlin Tuesdays: my life has been really hectic lately (and I just accepted another job offer, so I may well be moving again!). Please bear with me while I try to make get my life a little more stable!
Next week, we explore Kotlin with a Spring Framework. I personally haven’t tried web development with Kotlin, but this is one I’m REALLY excited for. Stay tuned for next week!
Tags:
Codetown is a social network. It's got blogs, forums, groups, personal pages and more! You might think of Codetown as a funky camper van with lots of compartments for your stuff and a great multimedia system, too! Best of all, Codetown has room for all of your friends.
Created by Michael Levin Dec 18, 2008 at 6:56pm. Last updated by Michael Levin May 4, 2018.
Check out the Codetown Jobs group.

Google Cloud's Key Management Service now supports post-quantum Key Encapsulation Mechanisms (KEMs), addressing future threats from quantum computing. This update empowers organizations to prepare against "Harvest Now, Decrypt Later" attacks while ensuring long-term data confidentiality.
By Steef-Jan Wiggers
Anthropic has unveiled a new feature called Skills, designed to let developers extend Claude with modular, reusable task components.
By Daniel Dominguez
Introducing Amazon EC2 Capacity Manager: AWS's new centralized solution for streamlined monitoring and management of EC2 capacity across all accounts and regions. This innovative tool consolidates data into a unified dashboard, enhancing efficiency while reducing operational complexity.
By Steef-Jan Wiggers
During Cloudflare's recent Birthday Week, the company announced the private preview of its Cloudflare Email Service. This new globally managed service enables developers to send and receive emails directly from Workers using native bindings, without the need for API keys.
By Renato Losio
Meta’s PyTorch team has launched Monarch, a framework that simplifies distributed AI workflows across multiple GPUs and machines. It uses a single-controller model to manage computations across a cluster, making large-scale training and reinforcement learning tasks easier while allowing developers to keep their standard PyTorch coding practices.
By Robert Krzaczyński
© 2025 Created by Michael Levin.
Powered by