Radiance comes with a number of sample / demo apps that showcase the flexibility and power of its APIs. One of those demos is Lumen. Its main goal is to highlight the feature set of the Trident animation library. Lumen uses MusicBrainz JSON web service to search for all albums of the specific artist, and for the list of tracks on individual albums. Sending requests and parsing responses is done with Retrofit and Moshi. Lucent is the port of Lumen to Kotlin.

Let’s see how it works together in Kotlin.

We start by adding the build dependencies on Retrofit and Moshi:

dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
}

Next, we define our service interface that maps to MusicBrainz APIs:


    private interface MusicBrainzService {
        @GET("/ws/2/release?type=album&fmt=json")
        fun getReleases(@Query("artist") artistId: String): Call

        @GET("/ws/2/release/{release}?inc=recordings&fmt=json")
        fun getRelease(@Path("release") releaseId: String): Call

        companion object {
            const val API_URL = "https://musicbrainz.org/"
        }
    }

Note the usage of fmt=json attribute in all @GET functions, and usage of @Query and @Path that matches the expected endpoint contracts.

The data classes map to the matching MusicBrainz entities, using @field:Json annotation with the matching name attribute, along with @Json annotation on one of the data classes to properly map it to the matching JSON tags:


data class SearchResultRelease(
        @field:Json(name = "id") val id: String?,
        @field:Json(name = "title") val title: String?,
        @field:Json(name = "artist") var artist: String?,
        @field:Json(name = "date") val date: String?,
        @field:Json(name = "release-events") val releaseEvents: List,
        @field:Json(name = "asin") val asin: String?)

data class Area(
        @field:Json(name = "disambiguation") val disambiguation: String?,
        @field:Json(name = "id") val id: String?,
        @field:Json(name = "name") var name: String?,
        @field:Json(name = "sort-name") val sortName: String?,
        @field:Json(name = "iso-3166-1-codes") val iso31661Codes: List)

data class Medium(
        @field:Json(name = "tracks") val tracks: List)

data class Release(
        @field:Json(name = "id") val id: String?,
        @field:Json(name = "title") val title: String?,
        @field:Json(name = "date") val date: String?,
        @field:Json(name = "media") val media: List,
        @field:Json(name = "asin") val asin: String?)

data class ReleaseEvent(
        @field:Json(name = "date") val date: String?,
        @field:Json(name = "area") val area: Area?)

@Json(name = "release-list")
data class ReleaseList(
        @field:Json(name = "count") val count: Int?,
        @field:Json(name = "releases") val releases: List)

data class Track(
        @field:Json(name = "title") val title: String?,
        @field:Json(name = "length") val length: Int?)

Now we can create a Retrofit object and fire off our request:


        val retrofit = Retrofit.Builder()
                .baseUrl(MusicBrainzService.API_URL)
                .client(getHttpClient())
                .addConverterFactory(MoshiConverterFactory.create())
                .build()

        val service = retrofit.create(MusicBrainzService::class.java)

        val releaseResponse = service.getReleases(artistId).execute()
        val releases = releaseResponse.body()

And to get the list of tracks for the specific album:


    fun doTrackSearch(releaseId: String): List {
        val retrofit = Retrofit.Builder()
                .baseUrl(MusicBrainzService.API_URL)
                .client(getHttpClient())
                .addConverterFactory(MoshiConverterFactory.create())
                .build()

        val service = retrofit.create(MusicBrainzService::class.java)

        val releaseResponse = service.getRelease(releaseId).execute()
        val release = releaseResponse.body()

        return release!!.media[0].tracks
    }

Where the OkHttpClient is configured like this:


    private fun getHttpClient(): OkHttpClient {
        val okHttpBuilder = OkHttpClient.Builder()
        okHttpBuilder.addInterceptor { chain ->
            val requestWithUserAgent = chain.request().newBuilder()
                    .header("User-Agent", "My custom user agent")
                    .build()
            chain.proceed(requestWithUserAgent)
        }
        return okHttpBuilder.build()
    }

This is it. No messy handling of HTTP requests, no manual parsing of JSON responses. All driven by metadata and encapsulated by Kotlin data classes.

Radiance 2.5.0

September 3rd, 2019

It gives me great pleasure to announce the third major release of Radiance. Let’s get to what’s been fixed, and what’s been added. First, I’m going to use emojis to mark different parts of it like this:

💔 marks an incompatible API / binary change
😻 marks new features
🤷‍♀️ marks bug fixes and general improvements

Substance

  • 😻 New skins – Nebula Amethyst, Night Shade and Graphite Sunset
  • 🤷‍♀️ Fix for disappearing internal frame title pane buttons
  • 🤷‍♀️ Fix for crash during initialization
  • 🤷‍♀️ Fix for OutOfMemoryError on sliders with large model ranges
  • 🤷‍♀️ Fix for slider tracks under dark skins
  • 💔 Fix for incorrect tracking of state-based alpha values in color scheme bundles
  • 🤷‍♀️ Fix for drop shadows under some skins
  • 🤷‍♀️ Fix for contrast ratio of highlighted content under Sahara skin
  • 🤷‍♀️ Fix for antialiased rendering of pasted text content

Flamingo

Trident

  • 😻 DSL for Trident
  • 🤷‍♀️ Fix for combining looping timelines with .fromCurrent()

Photon

The first Radiance release focused on bringing all the different Swing open-source projects that I’ve been working on since 2005 under one roof. The second Radiance release was about making them work much better together. And this one (code-named Coral) is about covering major functionality gaps that were missing up until now.

There’s still a long road ahead to continue exploring the never-ending depths of what it takes to write elegant and high-performing desktop applications in Swing. If you’re in the business of writing just such apps, I’d love for you to take this third Radiance release for a spin. Click here to get the instructions on how to add Radiance to your Gradle / Maven / Ivy / Leiningen / Bazel builds. And don’t forget that all of the modules require Java 9 to build and run.

There’s a bunch of helper extension methods that the Kotlin standard library provides for working with collections. However, it would seem that at the present moment java.util.Enumeration has been left a bit behind. Here is a simple extension method to convert any List to a matching Enumeration:


/**
 * Extension function for converting a {@link List} to an {@link Enumeration}
 */
fun <T> List<T>.toEnumeration(): Enumeration<T> {
    return object : Enumeration<T> {
        var count = 0

        override fun hasMoreElements(): Boolean {
            return this.count < size
        }

        override fun nextElement(): T {
            if (this.count < size) {
                return get(this.count++)
            }
            throw NoSuchElementException("List enumeration asked for more elements than present")
        }
    }
}

And here is how you can use it to expose the local file system to the JTree component. First, we create a custom implementation of the TreeNode interface:


data class FileTreeNode(val file: File?, val children: Array<File>, val nodeParent: TreeNode?) : TreeNode {
    constructor(file: File, parent: TreeNode) : this(file, file.listFiles() ?: arrayOf(), parent)
    constructor(children: Array<File>) : this(null, children, null)

    init {
        children.sortWith(compareBy { it.name.toLowerCase() })
    }

    override fun children(): Enumeration<FileTreeNode> {
        return children.map { FileTreeNode(it, this) }.toEnumeration()
    }

    override fun getAllowsChildren(): Boolean {
        return true
    }

    override fun getChildAt(childIndex: Int): TreeNode {
        return FileTreeNode(children[childIndex], this)
    }

    override fun getChildCount(): Int {
        return children.size
    }

    override fun getIndex(node: TreeNode): Int {
        val ftn = node as FileTreeNode
        return children.indexOfFirst { it == ftn.file }
    }

    override fun getParent(): TreeNode? {
        return this.nodeParent
    }

    override fun isLeaf(): Boolean {
        val isNotFolder = (this.file != null) && (this.file.isFile)
        return this.childCount == 0 && isNotFolder
    }
}

Note a few language shortcuts that make the code more concise than its Java counterparts:

  • Since File.listFiles() can return null, we wrap that call with a simple Elvis operator: file.listFiles() ?: arrayOf().
  • The initializer block sorts the File children in place by name.
  • To return tree node enumeration in children(), we first map each File child to the corresponding FileTreeNode and then use our extension function to convert the resulting List to Enumeration.
  • Looking up the index of the specific node is done with the existing extension indexOfFirst function from the standard library.

Now all is left to do is to create our JTree:


val tree = JTree(FileTreeNode(File.listRoots()))
tree.rootVisible = false

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.3.9. 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.3.9.