This article presents a user interface based on a Composable View. The Composable View - implemented as a TornadoFX View subclass - creates and contains two subviews which are implemented as TornadoFX Fragment subclasses. Model-View-ViewModel (MVVM) techniques are used to keep the three parts in sync: View and two Fragments.
TornadoFX is a Kotlin JavaFX framework that supports an MVVM design and provides syntactic sugar to reduce boilerplate needed to wire such an app.
The program starts with an empty screen.
Pressing the Refresh Button will initiate a data retrieval operation from a RESTful web service. As the data is loading, a ProgressBar and Label is displayed.
Once the data is retrieved, the ProgressBar and Label are dismissed (actually their parent HBox is hidden). Three data elements from the service -- US Presidents -- are added to the ListView. Selecting the second item, Abe Lincoln, fills the details panel to the right.
The main entry point is a TornadoFX App subclass CustomerApp blends the JavaFX Application, Stage, and Scene objects. The App subclass is initialized with a View subclass MainView. MainView provides a root node for the Scene of the App.
MainView is further decomposed into a pair of Fragments, CustomerListFragment and CustomerDetailsFragment. Additionally, MainView manages several UI controls of its own: a ProgressBar, a status Label, and an HBox container.
This programs UI is simple, but the composable structure lends itself to a scalable design. That is, the MainView references the child Fragments, yet does not interact with them directly. This means that the Fragments themselves can be further composed of additional Fragments without modifying the existing parts.
The key to managing the complexity in the UI is to build in layers.
class CustomerApp : App(MainView::class) {
val api : Rest by inject()
override fun createPrimaryScene(view: UIComponent) =
Scene(view.root, 568.0, 320.0 )
init {
api.baseURI = "https://www.bekwam.net/data"
}
}
class MainView : View("Customer App") {
val customerViewModel : CustomerViewModel by inject()
override val root =
vbox {
splitpane {
this += find(CustomerListFragment::class)
this += find(CustomerDetailsFragment::class)
padding = Insets(4.0)
}
hbox {
progressbar {
progressProperty().bind( customerViewModel.taskProgress )
}
label {
textProperty().bind( customerViewModel.taskMessage )
}
visibleProperty().bind( customerViewModel.taskRunning )
padding = Insets(4.0)
spacing = 4.0
}
}
}
data class Customer(val id : Int, val firstName : String, val lastName : String) {
override fun toString(): String {
return "$firstName $lastName";
}
}
class CustomerListFragment : Fragment() {
val customerViewModel : CustomerViewModel by inject()
override val root = vbox {
button("Refresh") {
setOnAction { refresh() }
}
listview<Customer> {
itemsProperty().bind( customerViewModel.customers )
selectionModel.selectedItemProperty().addListener {
obs, ov, nv -> updateSelected(nv)
}
}
padding = Insets(10.0)
spacing = 4.0
}
fun refresh() {
customerViewModel.refresh()
}
fun updateSelected(newSelection : Customer?) {
customerViewModel.updateSelected(newSelection)
}
}
object CustomerModelUpdated : FXEvent() // thrown Model -> ViewModel
class CustomerDetailsFragment : Fragment() {
val customerViewModel : CustomerViewModel by inject()
override val root = vbox {
label("First Name")
textfield {
textProperty().bind( customerViewModel.selectedFirstName )
}
label("Last Name")
textfield {
textProperty().bind( customerViewModel.selectedLastName )
}
padding = Insets(10.0)
}
}
class CustomerViewModel : ViewModel() {
val customerModel : CustomerModel by inject()
val customers = SimpleObjectProperty<ObservableList<Customer>>()
val selectedFirstName = SimpleStringProperty()
val selectedLastName = SimpleStringProperty()
val taskRunning = SimpleBooleanProperty()
val taskMessage = SimpleStringProperty()
val taskProgress = SimpleDoubleProperty()
init {
subscribe<CustomerModelUpdated> {
updateFromRefresh()
}
}
fun refresh() {
val t = object : Task<Unit>() {
override fun call() {
updateMessage("Loading customers")
updateProgress( 0.4, 1.0 )
customerModel.loadCustomers()
}
override fun succeeded() {
fire( CustomerModelUpdated );
}
}
taskRunning.bind( t.runningProperty() )
taskProgress.bind( t.progressProperty() )
taskMessage.bind( t.messageProperty() )
Thread(t).start()
}
fun updateSelected(newSelection : Customer?) {
if( newSelection == null ) {
selectedFirstName.set("")
selectedLastName.set("")
} else {
selectedFirstName.set( newSelection!!.firstName )
selectedLastName.set( newSelection!!.lastName )
}
}
fun updateFromRefresh() {
customers.set(
customerModel.customers.observable()
)
}
}
class CustomerModel : Controller() {
val api : Rest by inject()
val customers = mutableListOf<Customer>()
fun loadCustomers() {
customers.clear()
api.get("customers.json")
.list()
.forEach{
val obj = it as JsonObject
customers.add(
Customer(
obj.getInt("id"),
obj.getString("firstName"),
obj.getString("lastName")
)
)
}
fire( CustomerModelUpdated )
}
}
The source code presented in this video series is an IntelliJ IDEA project found in the zip file below.
January 6, 2017 / Carl Walker /
Categories: kotlin,splitpane,mvvm,javafx,architecture