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.
Microsoft is expanding .NET developers’ toolset with enhancements to Code Optimizations. This feature is part of Azure Monitor offering and now works with the .NET Profiler in Application Insights to automatically detect CPU, memory, and threading issues in production apps and give code‑level recommendations to fix them.
By Edin KapićSam Cox shares a case study on building a startup platform with C#. He explains how C#’s modern, open-source ecosystem, integrated tooling, and robust libraries enabled him to achieve high developer productivity, rapid iteration, and overcome significant performance challenges, ultimately helping the company secure its first paying customer.
By Sam CoxHugging Face has introduced a new integration that allows developers to connect Inference Providers directly with GitHub Copilot Chat in Visual Studio Code. The update means that open-source large language models — including Kimi K2, DeepSeek V3.1, GLM 4.5, and others — can now be accessed and tested from inside the VS Code editor, without the need to switch platforms or juggle multiple tools.
By Robert KrzaczyńskiOracle has released version 25 of the Java programming language and virtual machine. As the first LTS release since JDK 21, the final feature set includes 18 JEPs, seven of which are finalized having evolved through the incubation and preview processes. Nine of these features are focused on performance and runtime.
By Michael RedlichDylan Fox discusses how accessibility drives innovation in extended reality. Learn how the "curb cut effect" applies to XR development, leading to advancements like AI agents, novel inputs, and multisensory experiences that improve user experience for everyone, not just those with disabilities.
By Dylan Fox
© 2025 Created by Michael Levin.
Powered by