Loading scene

With JavaFx, there are two ways of defining UI elements. One is using Kotlin (or Java) code to create UI control class instances and then styling them and positioning them programmatically. The other way is using FXML, which is an XML-based markup language.

With FXML, we can separate the presentation part of our app from the application logic part and that's why we'll use it for our UI.

FXML doesn't have a schema, it maps directly to JavaFx control classes. This means that you don’t have to learn FXML, you can look into the JavaFx documentation and see what controls are available and what properties they offer. The full documentation of the API is available here: https://docs.oracle.com/javase/8/javafx/api/toc.htm.

Let's start with the loading screen. In the resources folder of our project, add a new file called loader.fxml. Here, we'll define our loading scene. The scene is pretty simple; we need two controls, a label to display that we're loading the data, and a progress indicator:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>

<VBox fx:controller="com.packt.kotlinquickstart.dictionary.LoadingController"
alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.121"
xmlns:fx="http://javafx.com/fxml/1">
<Label alignment="CENTER" text="Loading Dictionary Data ..."
textAlignment="JUSTIFY">
<font>
<Font name="Arial" size="19.0"/>
</font>
<VBox.margin>
<Insets bottom="20.0"/>
</VBox.margin>
</Label>
<ProgressIndicator>
<opaqueInsets>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</opaqueInsets>
</ProgressIndicator>
</VBox>

At the top of the file, you can see how all the controls used in the file are first imported. The imports use the full class name, including the package name. If you know the name of the control you wish to use, you can start typing the name and IntelliJ will auto-complete and insert the import statement for you.

The root of our scene is a VBox control, which lays its children out inside a single vertical column. The root control also has the fx:controller attribute, which has a class that we don't have yet. This class will be our controller for this UI scene. Controllers can manage the UI and define the logic for our scene.

Now, back in the MainApp class, we'll add a function for changing scenes:

private fun changeScene(fxml: String, title: String = "Dictionary") {
val page = FXMLLoader.load(javaClass.classLoader.getResource(fxml)) as Parent
var scene = stage.scene
stage.title = title
if (scene == null) {
scene = Scene(page, 900.0, 700.0)
stage.scene = scene
} else {
stage.scene.root = page
}
stage.sizeToScene()
}

The function accepts the name of an FXML file and optionally a title for the scene. The whole UI control hierarchy can be created from an FXML file with the load function call to the FXMLoader class. When the scene is loaded, it is placed as the root of the stage our app was created with.

While we are still in the MainApp class, we can add a function that our LoadingController will invoke when it is done loading the data. We will then navigate to the main scene:

fun onDictionaryLoaded() {
changeScene("main.fxml")
}

We can also add the main.fxml file to the resources folder and leave it empty for now.

Our LoadingController will also need an instance of MainApp so it can invoke the onDictionaryLoaded function. We can add a MainApp property to the companion object:

companion object {
lateinit var instance: MainApp
}

And then, add a constructor where we initialize this property:

init {
instance = this
}

We have everything we need to finish the MainApp class. The stage property is the one that is provided by the JavaFx in the start function. We store it so we can change the scenes in the changeScene function:

private lateinit var stage: Stage

And finally, we have the start function:

override fun start(primaryStage: Stage) {
stage = primaryStage

val repository = Repository()
repository.onStart()
if (repository.isDataLoaded()) {
changeScene("main.fxml")
} else {
changeScene("loader.fxml")
}

primaryStage.show()
}

We check we have loaded the data; if not, we navigate to the loader scene and if yes, we go straight to the main scene.

To finish the loading scene, we’ll need to implement the controller class for it. But before we do that, we can create an implementation of the Java executor interface that schedules actions on the main thread of our JavaFx App. We'll use it on the CompletableFuture API, so we can schedule actions that interact with the UI on the main thread of our app. Executing a Runnable on the main JavaFx thread is done with the Platform.runLater method call:

class MainThreadExecutor private constructor() : Executor {

override fun execute(command: Runnable) {
Platform.runLater(command)
}

companion object {
private val instance = MainThreadExecutor()

val INSTANCE
get() = instance
}
}

We can get by with only one instance of this class, so we made it a singleton. A private constructor ensures that we can construct an instance from inside the class and we do that inside a companion object, which also exposes a public instance property.

Finally, there is the LoadingController implementation. An instance of this class will be created by JavaFx when the Loading scene gets initialized. That's why the class needs to have a parameterless constructor.

The logic of the controller is really simple: it has to load the data into the database and then notify MainApp when it is done. Here's the full code of the controller:

class LoadingController {

private val loader = DictionaryLoader(EntriesIterator())

@FXML
fun initialize() {
loader.loadDictionary()
.thenRunAsync(Runnable { MainApp.instance.onDictionaryLoaded() }
, MainThreadExecutor.INSTANCE)
.handle { _, fail -> if (fail != null) Platform.runLater { onError(fail) } }
}

private fun onError(fail: Throwable) {
val alert = Alert(Alert.AlertType.ERROR)
alert.headerText = "An error occurred!"
alert.dialogPane.expandableContent = ScrollPane(TextArea(fail.message))
alert.showAndWait()
}
}

If you define a function called initialize and apply the FXML annotation to it, then JavaFx will invoke it when it has finished loading all the UI controls. In our case, we start loading dictionary data into that function.

The loadDicationary function returns CompletableFuture. We want to notify MainApp we are done loading the dictionary, so we schedule a completion action for this CompletableFuture with a thenRunAsync call. We schedule it on the main thread of our app with the MainThreadExecutor we created in the previous section.

If something goes wrong while we're loading the data, we want to notify the user about it. That's why added the handle function call, which will be invoked if an exception is thrown.

If we now run the app, you should see our loading scene while the dictionary database is being populated:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.88.62