Note: code samples in this post are kept in sync with the latest stable Kotlin coroutines release. All the samples have been verified to work with release 1.10.2. Also see the official guide to UI programming with coroutines.

Kotlin 1.3 has been released, and one of the major additions to the language is the official graduation of coroutines from experimental to stable. Let’s take a look at how we can replace the old and venerable SwingWorker with something that is a bit more modern.

In the simplest use case, you do some kind of long-running work in your doInBackground() and then process the result of that work in done():


object : SwingWorker<List<StringValuePair>, Void>() {
    @Throws(Exception::class)
    override fun doInBackground(): List<StringValuePair>? {
        return bar.callback.getLeafs(newPath)
    }

    override fun done() {
        try {
            filePanel.setFolder(get())
        } catch (exc: Exception) {
        }
    }
}.execute()

There’s a whole bunch of noise, so to speak, around the core of the logic – calling breadcrumb bar’s callback to get the leaf content for the newly set path, and then populating the folder based on the retrieved content. Let’s see how this code can look like with coroutines:


filePanel.setFolder(GlobalScope.async {
    bar.callback.getLeafs(newPath)
}.await())

In this case, we have distilled the core of the logic flow to its essence – asynchronous loading of the leaf content, followed by updating the UI. Another option is to collapse the async / await block into a single withContext to change the execution context away from the UI thread:


filePanel.setFolder(withContext(Dispatchers.Default) {
    bar.callback.getLeafs(newPath)
})

Note that once you switch to coroutines, you also need a larger context for proper synchronization. In Swing, it means wrapping the entire listener with GlobalScope.launch(Dispatchers.Swing):


// Configure the breadcrumb bar to update the file panel every time
// the path changes
bar.model.addPathListener {
    GlobalScope.launch(Dispatchers.Swing) {
        val newPath = bar.model.items
        if (newPath.size > 0) {
            // Use the Kotlin coroutines (experimental) to kick the
            // loading of the path leaf content off the UI thread and then
            // pipe it back to the UI thread in setFolder call.
            filePanel.setFolder(withContext(Dispatchers.Default) {
                bar.callback.getLeafs(newPath)
            })
        }
    }
}

Now let’s take a look at something a bit more interactive – updating the UI on the ongoing progress of a long-running background task.

Let’s add a button and a label to a sample frame, and kick off a 5-second task that updates the UI every second on the progress using SwingWorker.process():


val button = JButton("Start operation!")
val status = JLabel("Progress")

frame.add(button)
frame.add(status)

button.addActionListener {
    class MyWorker : SwingWorker<Unit, Int>() {
        override fun doInBackground() {
            for (i in 1..5) {
                publish(i)
                Thread.sleep(1000)
            }
        }

        override fun process(chunks: MutableList<Int>?) {
            status.text = "Progress " + chunks?.joinToString()
        }

        override fun done() {
            status.text = "Done!"
        }
    }

    val worker = MyWorker()
    worker.execute()
}

The first way to convert to coroutines would be with the help of channels:


button.addActionListener {
    GlobalScope.launch(Dispatchers.Swing) {
        val channel = Channel<Int>()
        GlobalScope.launch {
            for (x in 1..5) {
                println("Sending $x " + SwingUtilities.isEventDispatchThread())
                // This is happening off the main thread
                channel.send(x)
                // Emulating long-running background processing
                delay(1000L)
            }
            // Close the channel as we're done processing
            channel.close()
        }
        // The next loop keeps on going as long as the channel is not closed
        for (y in channel) {
            println("Processing $y " + SwingUtilities.isEventDispatchThread())

            status.text = "Progress $y"
        }
        status.text = "Done!"
    }
}

Note the usage of Dispatchers.Swing context that is passed to the GlobalScope.launch() function and the wrapping of the emulated long-running task in another GlobalScope.launch lambda. Then, as long as that lambda keeps on sending content into the channel, the for loop iterates over the channel content on the UI thread, and then relinquishes the UI thread so that it is no longer blocked.

Now let’s make it a little bit more structured. While code samples found in documentation and tutorials run on the lighter side of things (including this article), real-life apps would have more complexity on both sides of async processing. We’re going to split this logic into two parts – background processing of data and updating the UI on the main thread. Background processing becomes a separate function that returns a data channel:


fun process() : ReceiveChannel<Int> {
    val channel = Channel<Int>()
    GlobalScope.launch {
        for (x in 1..5) {
            println("Sending $x " + SwingUtilities.isEventDispatchThread())
            // This is happening off the main thread
            channel.send(x)
            // Emulating long-running background processing
            delay(1000L)
        }
        // Close the channel as we're done processing
        channel.close()
    }
    return channel
}

And UI code consumes the data posted to the channel and updates the relevant pieces:


button.addActionListener {
    GlobalScope.launch(Dispatchers.Swing) {
        // The next loop keeps on going as long as the channel is not closed
        for (y in process()) {
            println("Processing $y " + SwingUtilities.isEventDispatchThread())

            status.text = "Progress $y"
        }
        status.text = "Done!"
    }
}

Let’s make the background task cancellable so that the currently running operation can be safely canceled without subsequent erroneous UI updates. Background processing returns an object that has two parts – data channel and a job that can be canceled if needed:


class ProcessResult(val resultChannel: ReceiveChannel<Int>, val job: Job)

fun processCancelable() : ProcessResult {
    val channel = Channel()
    val job = GlobalScope.launch {
        for (x in 1..5) {
            if (!isActive) {
                // This async operation has been canceled
                break
            }
            println("Sending $x " + SwingUtilities.isEventDispatchThread())
            // This is happening off the main thread
            channel.send(x)
            // Emulating long-running background processing
            delay(1000L)    
        }
        // Close the channel as we're done processing
        channel.close()
    }
    return ProcessResult(channel, job)
}

And on the UI side of things, we keep track of the last job that was kicked off and cancel it:


var currJob: Job? = null

button.addActionListener {
    GlobalScope.launch(Dispatchers.Swing) {
        currJob?.cancel()

        val processResult = processCancelable()
        currJob = processResult.job

        // The next loop keeps on going as long as the channel is not closed
        for (y in processResult.resultChannel) {
            println("Processing $y " + SwingUtilities.isEventDispatchThread())

            status.text = "Progress $y"
        }
        status.text = "Done!"
    }
}

Finally, we can move away from the existing concept of data communication “pipe” between the two parts, and start thinking in terms of passing a lambda to be invoked by the data producer when it has completed processing the next chunk of data. In this last example, the producer marks itself with the suspend keyword and uses the parent context so that cancellation is properly propagated:


suspend fun processAlternative(job : Job, progress: (Int) -> Unit = {}) {
    for (x in 1..5) {
        // Emulating long-running background processing.
        // Use the parent job so that cancellation of the parent propagates in here.
        GlobalScope.async(context=job) {
            println("Running on " + SwingUtilities.isEventDispatchThread())
            delay(1000L)
        }.await()
        // And calling the callback on the UI thread
        println("Sending $x " + SwingUtilities.isEventDispatchThread())
        progress(x)
    }
}

And the UI side of things supplies a lambda that updates the relevant UI pieces:


var currJob: Job? = null

button.addActionListener {
    currJob?.cancel()

    currJob = GlobalScope.launch(Dispatchers.Swing) {
        // This will run until all the sequential async blocks are done
        processAlternative(currJob!!) { progress ->
            println("Processing $progress " + SwingUtilities.isEventDispatchThread())

            status.text = "Progress $progress"
        }
    status.text = "Done!"
    }
}

If you want start playing with replacing SwingWorker with coroutines, add org.jetbrains.kotlinx:kotlinx-coroutines-swing and org.jetbrains.kotlinx:kotlinx-coroutines-core to your dependencies. The latest version for both of the modules is 1.10.2.

Hello Radiance

May 23rd, 2018

This week marks the beginning of a new phase for a bunch of my long-running open-source Swing projects. Some of them have started all the way back in 2005, and some have joined later on along the road. Over the years, they’ve been hosted on three sites (java.net, kenai.com and github.com) in three version control systems (cvs, svn, git). Approaching the 15th year mark (with a hiatus along the way), it’s clear that it’s time to revisit the fundamental structure of these projects and bring them into a more modern world.

Since these projects have been brought back to life in the last two years, the entire codebase has been revisited to clean up the cruft that has accumulated over time. Some of the explorations that I’ve embarked on have not went as well as I hoped they would be. That has been the fate of laf-plugin and laf-widget projects that aimed to bring common functionality across a variety of third-party look-and-feels, a field that is only seeing Substance and Synthetica as the two lone survivors.

Today I’m happy to announce the beginning of Project Radiance, the new umbrella brand that will unify and streamline the way Swing developers can integrate my libraries into their projects. At a high-level:

  • Radiance is a single project that provides a Gradle-based build that no longer relies on knowing exactly what to check out and where the dependent projects need to be located. It also uses proper third-party project dependencies to pull those at build time.
  • Starting from the very first release (planned for second half of 2018), Radiance will provide Maven artifacts for all core libraries – Trident (animation), Substance (look-and-feel), Flamingo (components), Ibis (SVG icons) and others.
  • The Kormorant sub-project is the first exploration into using Kotlin DSLs (domain-specific languages) for more declarative way of working with Swing UIs.
  • Flamingo components will only support Substance look-and-feel, no longer doing awkward and unnecessary tricks to try and support core and other third-party look-and-feels.

All the open bugs on existing GitHub projects have been migrated to be under Radiance. Once the migration of all the relevant documentation and older binaries to Radiance is complete in the next couple of weeks, those projects will be deleted from GitHub.

Starting from today, all new development such as bug fixes, feature work and documentation updates will only be done under Radiance. The versioning of all the projects will be unified going forward, resetting to 1.0. Some public APIs might move between sub-projects (going into Neon).

5,736 pedestrians were killed in traffic crashes in USA in 2015. That is 15 people killed every day walking the roads.
37,461 people were killed in traffic crashes overall in USA in 2016. That is 102 people killed every day being on the roads. Additional (staggering) 2M+ were injured or permanently disabled.

There is nothing in the constitution, the bill of rights or the other amendments that guarantees an inalienable right for citizens, residents and other individuals to possess and operate a steel box at speeds that simply do not match our abilities to react in time to whatever may happen on the road at any given moment.

And yet, there is no public uproar. There are no petitions. There are no mass walkouts. There are no social media hashtags. There are no somber politicians sending thoughts and prayers. There is no government agency combing the aftermath of every single crash that resulted in a fatality to make sure that something like that won’t ever happen again.

Nobody gets in the car weighing their chances and deciding that yes, that trip to see their favorite team playing some other team is certainly worth the chance to die today.

Imagine getting on the plane knowing that there’s a decent chance that you’re not going to make it to your destination. Imagine a passenger plane crash happening every four days. Taking 400 lives. Twice a week or so. Because that is what is happening on the roads in this country. And every other country. More than 1.25 million people die every year world wide as a result of road traffic crashes.

And yet, there is no anger towards some kind of an organization that promotes the interests of big car manufacturers. Nobody is thinking to ostracize their friends for buying that shiny new car that can accelerate from 0 to 100 faster than ever before. There are no voices calling to raise the minimum driving age for bigger SUVs to, let’s say 21.

And here is where it gets really difficult. If self-driving / majorly-assisted technology could bring those numbers down, but not quite to zero, what would be deemed acceptable? Setting aside the juicy lawsuit targets and the initial wave of breathless headlines and rhetoric of the last few days, how little is still too much?

What happens when it’s no longer the weak excuse of “it’s fine because it was this frail human who lost their concentration for a second”? What happens when we are talking about machines of unimaginable complexity hurtling ever faster down our roads, as they take human lives on the monthly, weekly, daily or even hourly basis?

The advocates of fully self-driving future seem to never quite talk about this, pretending that somehow everything is going to be peachy and there are not going to be any human lives lost from some point going forward into eternity. This week is a rude wake-up call to regroup and start thinking about this ugly side of mass ground transportation.

Releases 2018.H1

March 15th, 2018

Going with the biannual release cycle of my Swing projects, it’s time to do latest release batch.

Substance 8.0 (code-named Wyoming) is a major release that addresses technical debt accumulated in the API surface over the years and takes a major step towards enabling modern UI customizations for Swing applications. Full release notes and API listings are available, with the highlights being:

  • Unified API surface (Project Cerebrum)
  • Configurable title pane content (Project Visor)
  • Folded laf-plugin / laf-widget (Project Corpora)
  • Explicit instantiation of component and skin plugins
  • Switch to Material icons + icon pack support
  • Better support for fractional scaling factors

Flamingo 5.3 (code-named Liadan) has extracted the non-core functionality into two new projects:

  • Ibis has the code for using vector-based icons in Swing apps. It supports offline transcoding of SVG content into Java2D-powered classes, as well as dynamic display of SVG content at runtime (powered by the latest version of Apache Batik)
  • Spoonbill has the code for browsing SVN repositories with the JBreadcrumbBar component from the core Flamingo project. Future plans include extending this functionality to GitHub repositories as well.

If you’re in the business of writing Swing desktop applications, I’d love for you to take the latest releases of Substance and Flamingo for a spin. You can find the downloads in the /drop folders of the matching Github repositories. All of them require Java 8 to build and run.