DEV Community

Dominik Liebler
Dominik Liebler

Posted on • Originally published at domnikl.github.io on

Kotlin coroutines and JavaFX threads

On October 29, 2018 JetBrains released a new version of Kotlin (1.3) that included a long-awaited feature of the language: coroutines. Contrary to Go’s goroutine construct, they are not implemented as a language feature. Instead they are developed as a pure library implementation written in Kotlin. But apart from that they share a lot of similiarites with goroutines.

Behind the scenes, coroutines are very lightweight because they are being multiplexed on a few threads and creating one is much faster than spawning a new thread.

You can learn more about them over at JetBrains’ tutorial: “Your first coroutine with Kotlin”. Here I want to show you the caveat that you might encounter in a JavaFX applications when using coroutines.

So this is my basic JavaFX application written in Kotlin:

class MyApp : Application() {
    private val button = Label().also { it.text = "waiting for 0s" }

    override fun start(primaryStage: Stage?) {
        val pane = Pane().also {
            it.children.add(button)
        }

        primaryStage?.scene = Scene(pane,200.0,200.0)
        primaryStage?.show()
    }
}

fun main() {
    launch(MyApp::class.java)
}
Enter fullscreen mode Exit fullscreen mode

Nothing special here, just a GUI with a label that shows the text waiting for 0s. Now we want to extend our application to continuously update the label to always tell me the exact time I waited.

class MillisElapsedCounter(private val observer: Observer) {
    fun start() {
        GlobalScope.launch(Dispatchers.Default) {
            var millisElapsed = 0

            while (true) {
                delay(1000)
                millisElapsed += 1000
                observer.notify(millisElapsed)
            }
        }
    }

    interface Observer {
        fun notify(millisElapsed: Int)
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that you need to add a dependency to org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1 to be able to run this (remember: it’s just a library!)

So this is the implemention of the MillisElapsedCounter class and a corresponding Observer that is being notified every 1000ms. Clients of this class have to instantiate it providing an observer. Calling start() will then launch a new coroutine using the default dispatcher (there is a pool of dispatchers - which are threads behind the scenes - you can use) and return immediately. Note that delay(1000) is a suspend function, it will suspend execution in the thread it runs in. Running that in the application thread would freeze the UI forever and would be a bad idea.

To use the counter we also need to update MyApp to implement MillisElapsedCounter.Observer’s notify(millisElapsed: Int) and start the counter like this:

class MyApp : Application(), MillisElapsedCounter.Observer {
    private val button = Label().also { it.text = "waiting for 0s" }

    override fun start(primaryStage: Stage?) {
        MillisElapsedCounter(this).start()
        ...
    }

    override fun notify(millisElapsed: Int) {
        button.text = "waiting for ${millisElapsed / 1000}s"
    }
}
Enter fullscreen mode Exit fullscreen mode

Now if you run that, it will throw a java.lang.IllegalStateException exception that is coming from JavaFX:

Exception in thread “DefaultDispatcher-worker-3” java.lang.IllegalStateException: Not on FX application thread; currentThread = DefaultDispatcher-worker-3

To prevent from concurrency issues in the state of the UI, JavaFX prevents other threads from changing it. Look at the main() function that uses launch(MyApp::class.java) to start the application. This starts a new thread called JavaFX-Launcher which in turn starts the JavaFX Application Thread which runs all the UI related stuff. This one is the only thread allowed to make changes in the UI at runtime.

This means that we need to execute the changes that are being made to button.text in this very thread. The Kotlin team implemented another library for this sole purpose: org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.1.1 provides a new Dispatcher Dispatchers.JavaFx that uses the JavaFX Application Thread (there’s also -android and -swing to be used correspondingly).

Then the solution to our problem here is to use the correct Dispatcher Dispatchers.JavaFx and to implement another interface in MyApp called CoroutineScope:

class MyApp : Application(), MillisElapsedCounter.Observer, CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.JavaFx
        ...
Enter fullscreen mode Exit fullscreen mode

As it is now a CoroutineScope we can launch new coroutines directly from notify() using the correct scope:

override fun notify(millisElapsed: Int) {
    launch {
        button.text = "waiting for ${millisElapsed / 1000}s"
    }
}
Enter fullscreen mode Exit fullscreen mode

And that’s it! The exception will no longer occur and everything will run as expected - I waited a long time, and it still counts happily the seconds I waited :)

One thing we can improve though is that MillisElapsedCounter runs a coroutine in the GlobalScope which means that you need to keep track of it’s lifetime (if you were interested in that - eg. stopping it). More details on that in The reason to avoid GlobalScope. To fix that we could implement the interface CoroutineScope in MillisElapsedCounter as well:

class MillisElapsedCounter(private val observer: Observer) : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default

    fun start() {
        launch { ... }
    }
    ...
Enter fullscreen mode Exit fullscreen mode

We now have two coroutines cleanly separated from each other and each running in it’s own scope. A clean and simple solution I think.

The complete source code is available on GitHub, so you can clone it and play around with it:

You can read more about that in Guide to UI programming with coroutines.

Latest comments (0)