DEV Community

Cover image for From Swing to Compose Desktop #3

From Swing to Compose Desktop #3

Thomas Künneth
Developer. Speaker. Listener. Loves writing. GDE Android. Confessing mobile computing addict ;-)
Updated on ・3 min read

Welcome to the third post about my journey of transforming a Java Swing app to Compose for Desktop. Today I will cover the actual search for duplicates. Before we start: I found out that the official name is Compose for Desktop with no Jetpack prefix. I left the previous posts unchanged, but from now on will use the correct name. 😀

The old Swing user interface calls the business logic like this:

public void setupContents() {
    df.scanDir(textfieldBasedir.getText(), true);
    checksums = df.getChecksums();
Enter fullscreen mode Exit fullscreen mode

df and checksums are simple member variables.

private TKDupeFinder df = new TKDupeFinder();
private String[] checksums = {};
Enter fullscreen mode Exit fullscreen mode

The Swing code for updateGUI() looks lie this:

private void updateGUI() {
    boolean enabled = checksums.length > 1;
    currentPos = 0;
Enter fullscreen mode Exit fullscreen mode

To understand what's going on, please recall how the old app looks like:

The old app

Files are assumed duplicates if they share the same MD5 hash. That's what is stored in checksums. buttonPrev and buttonNext represent the small arrow buttons, which allow you to browse through the checksums. Each checksum refers to a list of Files. The mapping takes place in a method called updateContents().

private void updateContents(int offset) {
    if (checksums.length < 1) {
        labelInfo.setText("keine Dubletten gefunden");
    } else {
        currentPos += offset;
        if (currentPos >= checksums.length) {
            currentPos = 0;
        } else if (currentPos < 0) {
            currentPos = checksums.length - 1;
        List<File> files = df.getFiles(checksums[currentPos]); -> {
        labelInfo.setText(Integer.toString(currentPos + 1) + " von "
                + Integer.toString(checksums.length));
    listFiles.getSelectionModel().setSelectionInterval(1, modelFiles.getSize() - 1);
Enter fullscreen mode Exit fullscreen mode

So how does this translate to our new Kotlin code?

A very important variable is df. For the sake of simplicity I declare it top-level:

private val df = TKDupeFinder()
Enter fullscreen mode Exit fullscreen mode

We also need to remember two new states, currentPos and checksums. Just like name I put them in the TKDupeFinderContent composable:

val currentPos = remember { mutableStateOf(0) }
val checksums = remember { mutableStateOf<List<String>>(emptyList()) }
Enter fullscreen mode Exit fullscreen mode

The are passed to some of my other composables, sometimes as a state (when that composable must alter the value), sometimes just the value (when it is used to display something). You may be asking why, regarding checksums, I do not just remember a mutable list and change its contents. That's because the old business logic returns a list after a search, so it is easier to replace the reference rather than update the mutable list by removing the old and adding the new contents.

FirstRow(name, currentPos, checksums)
SecondRow(currentPos, checksums.value.size)
ThirdRow(currentPos.value, checksums.value)
Enter fullscreen mode Exit fullscreen mode

Now, let's take a look at the composables. For the sake of readability I omit some unchanged code.

fun FirstRow(name: MutableState<TextFieldValue>,
             currentPos: MutableState<Int>,
             checksums: MutableState<List<String>>) {
    Row(  ) {
                onClick = {
                    df.scanDir(name.value.text, true)
                    currentPos.value = 0
                    checksums.value = df.checksums.toList()
                modifier = Modifier.alignByBaseline(),
                enabled = File(name.value.text).isDirectory
        ) {
Enter fullscreen mode Exit fullscreen mode

I guess the most interesting part here is inside onClick(). The search logic remains unchanged (invoking clear(), scanDir() and removeSingles(). But through changing currentPos and checksums I can nicely trigger a ui refresh.

Next is SecondRow:

fun SecondRow(currentPos: MutableState<Int>, checksumsSize: Int) {
    val current = currentPos.value
    Row(  ) {
        Button(onClick = {
            currentPos.value -= 1
                enabled = current > 0) {
        Button(onClick = {
            currentPos.value += 1
                enabled = (current + 1) < checksumsSize) {
        Text(text = if (checksumsSize > 0) {
            "${currentPos.value + 1} of $checksumsSize"
        } else "No duplicates found")
Enter fullscreen mode Exit fullscreen mode

currentPos is passed as a state, because button clicks need to alter it, whereas checksumsSize is not changed but used only for checks and output.

Finally, ThirdRow.

Until today the list simply showed three fixed texts. Now I present the duplicates like this:

fun ThirdRow(currentPos: Int, checksums: List<String>) {
    val scrollState = rememberScrollState()
            scrollState = scrollState,
            modifier = Modifier.fillMaxSize().padding(8.dp),
    ) {
        if (checksums.isNotEmpty())
            df.getFiles(checksums[currentPos]).forEach {
Enter fullscreen mode Exit fullscreen mode

Here, too, both arguments do not represent a remembered state but its value, because they are not altered.

This is how the app looks now:

TKDupeFinder displaying duplicates

We for sure can beautify the visuals of the list. That's a topic for a future post. The next thing I will cover is list handling. The old app has two buttons to view or delete duplicate files. I am curious how I will map this behavior to Material Design. So please stay tuned.

From Swing to Jetpack Compose Desktop #1
From Swing to Jetpack Compose Desktop #2

Discussion (0)