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 has released Android 15 Beta. This version brings loudness control, screen recording detection, edge-to-edge apps by default, improvements for satellite connectivity and OpenJDK core libraries, new SQLite Apis, and more.
By Diogo CarletoMicrosoft recently announced significant updates to the Govern section of its Cloud Adoption Framework (CAF) for Azure, enhancing cloud governance guidance across various domains, including identity, cost management, and AI, to support better organizations, from startups to large enterprises, in their cloud journey.
By Steef-Jan WiggersIn this podcast Shane Hastie, Lead Editor for Culture & Methods spoke to Cassie Shum, VP of Field Engineering at Relational AI, about the importance of empathy in engineering culture and the key elements of building a strong team.
By Cassandra ShumSergey Bykov discusses the concept of Durable Execution, with a real world example of how they used it to build the Control Plane for Temporal Cloud.
By Sergey BykovExpedia's Performance and Reliability team has recently open-sourced its container-startup-autoscaler (CSA). It is a Kubernetes controller leveraging the In-Place Update of Pod Resources feature to dynamically adjust CPU and/or memory resources of containers during startup based on user-defined startup/post-startup configurations.
By Claudio Masolo© 2024 Created by Michael Levin. Powered by