Lightweight JavaFX Framework for Kotlin
- Supports both MVC, MVP and their derivatives
- Dependency injection
- Type safe GUI builders
- Type safe CSS builders
- First class FXML support
- Async task execution
- EventBus with thread targeting
- Hot reload of Views and Stylesheets
- OSGi support
- REST client with automatic JSON conversion
- Zero config, no XML, no annotations
TornadoFX requires Kotlin 1.1.2 and jvmTarget 1.8. Make sure you update your IDE plugins (Kotlin + TornadoFX).
After updating IntelliJ IDEA, make sure your Kotlin target version is 1.1 (Project Settings -> Modules -> Kotlin -> Language Version / API Version)
Remember to update your build system to configure the jvmTarget
as well.
For Maven, you add the following configuration block to kotlin-maven-plugin
:
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
For Gradle, it means configuring the kotlinOptions
of the Kotlin compilation task:
compileKotlin {
kotlinOptions.jvmTarget= "1.8"
}
Failing to do so will yield errors about the compiler not being able to inline certain calls.
You also need a full rebuild of your code after a version upgrade. If you run into trouble, try to clean caches and restart IDEA (File -> Invalidate caches / Restart).
- Screencasts
- Book (EAP) We are gradually migrating all information from the Wiki into the Guide
- Wiki
- Slack
- User Forum
- Dev Forum
- Stack Overflow
- Documentation
- IntelliJ IDEA Plugin
- Example Application
- Maven QuickStart Archetype
- Changelog
mvn archetype:generate -DarchetypeGroupId=no.tornado \
-DarchetypeArtifactId=tornadofx-quickstart-archetype \
-DarchetypeVersion=1.7.5
<dependency>
<groupId>no.tornado</groupId>
<artifactId>tornadofx</artifactId>
<version>1.7.5</version>
</dependency>
compile 'no.tornado:tornadofx:1.7.5'
Configure your build environment to use snapshots if you want to try out the latest features:
<repositories>
<repository>
<id>snapshots-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
Snapshots are published every day at GMT 16:00 if there has been any changes.
Create a View
class HelloWorld : View() {
override val root = hbox {
label("Hello world")
}
}
Load the root node from HelloWorld.fxml
and inject controls by fx:id
class HelloWorld : View() {
override val root: HBox by fxml()
val myLabel: Label by fxid()
init {
myLabel.text = "Hello world"
}
}
Start your application and show the primary View
and add a type safe stylesheet
class HelloWorldApp : App(HelloWorld::class, Styles::class)
class Styles : Stylesheet() {
init {
label {
fontSize = 20.px
fontWeight = FontWeight.BOLD
backgroundColor += c("#cecece")
}
}
}
Start app and load a type safe stylesheet
Use Type Safe Builders to quickly create complex user interfaces
class MyView : View() {
private val persons = FXCollections.observableArrayList(
Person(1, "Samantha Stuart", LocalDate.of(1981,12,4)),
Person(2, "Tom Marks", LocalDate.of(2001,1,23)),
Person(3, "Stuart Gills", LocalDate.of(1989,5,23)),
Person(3, "Nicole Williams", LocalDate.of(1998,8,11))
)
override val root = tableview(persons) {
column("ID", Person::id)
column("Name", Person::name)
column("Birthday", Person::birthday)
column("Age", Person::age)
columnResizePolicy = SmartResize.POLICY
}
}
RENDERED UI
Create a Customer model object that can be converted to and from JSON and exposes both a JavaFX Property and getter/setter pairs:
import tornadofx.getValue
import tornadofx.setValue
class Customer : JsonModel {
val idProperty = SimpleIntegerProperty()
var id by idProperty
val nameProperty = SimpleStringProperty()
var name by nameProperty
override fun updateModel(json: JsonObject) {
with(json) {
id = int("id")
name = string("name")
}
}
override fun toJSON(json: JsonBuilder) {
with(json) {
add("id", id)
add("name", name)
}
}
}
Create a controller which downloads a JSON list of customers with the REST api:
class HelloWorldController : Controller() {
val api : Rest by inject()
fun loadCustomers(): ObservableList<Customer> =
api.get("customers").list().toModel()
}
Configure the REST API with a base URI and Basic Authentication:
with (api) {
baseURI = "http://contoso.com/api"
setBasicAuth("user", "secret")
}
Load customers in the background and update a TableView on the UI thread:
runAsync {
controller.loadCustomers()
} ui {
customerTable.items = it
}
Load customers and apply to table declaratively:
customerTable.asyncItems { controller.loadCustomers() }
Define a type safe CSS stylesheet:
class Styles : Stylesheet() {
companion object {
// Define css classes
val heading by cssclass()
// Define colors
val mainColor = c("#bdbd22")
}
init {
heading {
textFill = mainColor
fontSize = 20.px
fontWeight = BOLD
}
button {
padding = box(10.px, 20.px)
fontWeight = BOLD
}
val flat = mixin {
backgroundInsets += box(0.px)
borderColor += box(Color.DARKGRAY)
}
s(button, textInput) {
+flat
}
}
}
Create an HBox with a Label and a TextField with type safe builders:
hbox {
label("Hello world") {
addClass(heading)
}
textfield {
promptText = "Enter your name"
}
}
Get and set per component configuration settings:
// set prefWidth from setting or default to 200.0
node.prefWidth(config.double("width", 200.0))
// set username and age, then save
with (config) {
set("username", "john")
set("age", 30)
save()
}
Create a Fragment
instead of a View
. A Fragment
is not a Singleton
like View
is, so you will
create a new instance and you can reuse the Fragment in multiple ui locations simultaneously.
class MyFragment : Fragment() {
override val root = hbox {
}
}
Open it in a Modal Window:
find(MyFragment::class).openModal()
Lookup and embed a View
inside another Pane
in one go
add(MyFragment::class)
Inject a View
and embed inside another Pane
val myView: MyView by inject()
init {
root.add(myFragment)
}
Swap a View for another (change Scene root or embedded View)
button("Go to next page") {
action {
replaceWith(PageTwo::class, ViewTransition.Slide(0.3.seconds, Direction.LEFT)
}
}
Open a View in an internal window over the current scene graph
button("Open") {
action {
openInternalWindow(MyOtherView::class)
}
}