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.
AWS has launched the open-source Model Context Protocol (MCP) Servers, revolutionizing AI-powered code assistants. These servers enhance development speed and security, ensuring adherence to AWS best practices. With features like automated Infrastructure as Code and cost insights, MCP democratizes AWS expertise and empowers developers to optimize cloud solutions effortlessly.
By Steef-Jan WiggersAWS has recently announced the general availability of Amazon VPC Route Server. This new option simplifies dynamic routing in a VPC, allowing developers to advertise routing information via Border Gateway Protocol (BGP) from virtual appliances and dynamically update the VPC route tables associated with subnets and internet gateways.
By Renato LosioIan Hoffman discusses Slack's architectural evolution from workspace-centric to Unified Grid. He explains scaling challenges & Enterprise Grid complexities, and shares lessons learned during this significant architectural shift, drawing insightful parallels to the history of astronomy and emphasizing the importance of questioning foundational assumptions in software development.
By Ian HoffmanIn this podcast, Shane Hastie, Lead Editor for Culture & Methods, spoke with Trisha Gee about the challenges and importance of addressing flaky tests, their impact on developer productivity and morale, best practices for testing, and broader concepts of measuring and improving developer productivity.
By Trisha GeeStefania Chaplin and Azhir Mahmood explain how to navigate the complexities of AI in highly regulated industries. They discuss MLOps pipelines, data security, evolving legislation (GDPR, EU AI Act), and the critical frameworks for responsible, secure, and explainable AI. Learn practical prevention techniques, XAI methods, and future trends in AI for cybersecurity and beyond.
By Stefania Chaplin, Azhir Mahmood
© 2025 Created by Michael Levin.
Powered by