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.
Jules Damji discusses which infrastructure should be used for distributed fine-tuning and training, how to scale ML workloads, how to accommodate large models, and how can CPUs and GPUs be utilized?
By Jules DamjiGitHub has released two features to improve the security and resilience of repositories. The first feature allows Dependabot to run as a GitHub Actions workflow using hosted and self-hosted runners. The second release introduces the public beta of Artifact Attestations, simplifying how repository maintainers can generate provenance for their build artifacts.
By Matt CampbellMeta AI released Llama 3, the latest generation of their open-source large language model (LLM) family. The model is available in 8B and 70B parameter sizes, each with a base and instruction-tuned variant. Llama3 outperforms other LLMs of the same parameter size on standard LLM benchmarks.
By Anthony AlfordAzure Monitor is Microsoft's cloud monitoring service for gathering, visualizing, and analyzing telemetry data from applications, infrastructure, and networks. The company recently added a data collection capability in preview with the edge pipeline, which enables the collection and routing of telemetry data before it's sent to the cloud.
By Steef-Jan WiggersWebAssembly has expanded its scope from browsers to other domains like cloud and edge computing. It uses the WebAssembly Component Model (WCM) to enable seamless interaction between libraries from different programming languages, such as Rust, Python, and JavaScript, promoting a true polyglot programming environment.
By Matt Butcher© 2024 Created by Michael Levin. Powered by