From Swing to Compose Desktop #3
Tags: java, kotlin, ux
Published: 2020-11-30 12:10:45

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:

```java
public void setupContents() {
    df.clear();
    df.scanDir(textfieldBasedir.getText(), true);
    df.removeSingles();
    checksums = df.getChecksums();
    updateGUI();
}
```

`df` and `checksums` are simple member variables.

```java
private TKDupeFinder df = new TKDupeFinder();
private String[] checksums = {};
```

The Swing code for `updateGUI()` looks lie this:

```java
private void updateGUI() {
    boolean enabled = checksums.length > 1;
    buttonPrev.setEnabled(enabled);
    buttonNext.setEnabled(enabled);
    currentPos = 0;
    updateContents(0);
}
```

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

![The old app](https://dev-to-uploads.s3.amazonaws.com/i/x5glh8yi9xiatbq9seq7.png)

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 `File`s. The mapping takes place in a method called `updateContents()`. 

```java
private void updateContents(int offset) {
    modelFiles.removeAllElements();
    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]);
        files.stream().forEach((f) -> {
            modelFiles.addElement(f);
        });
        labelInfo.setText(Integer.toString(currentPos + 1) + " von "
                + Integer.toString(checksums.length));
    }
    listFiles.getSelectionModel().setSelectionInterval(1, modelFiles.getSize() - 1);
    updateButtons();
}
```

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:

```kotlin
private val df = TKDupeFinder()
```

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

```kotlin
val currentPos = remember { mutableStateOf(0) }
val checksums = remember { mutableStateOf<List<String>>(emptyList()) }
```

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.

```kotlin
FirstRow(name, currentPos, checksums)
SecondRow(currentPos, checksums.value.size)
ThirdRow(currentPos.value, checksums.value)
```

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

```kotlin
@Composable
fun FirstRow(name: MutableState<TextFieldValue>,
             currentPos: MutableState<Int>,
             checksums: MutableState<List<String>>) {
    Row( … ) {
        …
        Button(
                onClick = {
                    df.clear()
                    df.scanDir(name.value.text, true)
                    df.removeSingles()
                    currentPos.value = 0
                    checksums.value = df.checksums.toList()
                },
                modifier = Modifier.alignByBaseline(),
                enabled = File(name.value.text).isDirectory
        ) {
            Text("Find")
        }
    }
}
```

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`:

```kotlin
@Composable
fun SecondRow(currentPos: MutableState<Int>, checksumsSize: Int) {
    val current = currentPos.value
    Row( … ) {
        Button(onClick = {
            currentPos.value -= 1
        },
                enabled = current > 0) {
            Text("\u140A")
        }
        MySpacer()
        Button(onClick = {
            currentPos.value += 1
        },
                enabled = (current + 1) < checksumsSize) {
            Text("\u1405")
        }
        MySpacer()
        Text(text = if (checksumsSize > 0) {
            "${currentPos.value + 1} of $checksumsSize"
        } else "No duplicates found")
    }
}
```

`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:

```kotlin
@Composable
fun ThirdRow(currentPos: Int, checksums: List<String>) {
    val scrollState = rememberScrollState()
    ScrollableColumn(
            scrollState = scrollState,
            modifier = Modifier.fillMaxSize().padding(8.dp),
    ) {
        if (checksums.isNotEmpty())
            df.getFiles(checksums[currentPos]).forEach {
                Text(it.absolutePath)
            }
    }
}
```

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](https://dev-to-uploads.s3.amazonaws.com/i/78mgqya33esop26tyga5.png)

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](https://dev.to/tkuenneth/from-swing-to-jetpack-compose-desktop-1-12kf)
[From Swing to Jetpack Compose Desktop #2](https://dev.to/tkuenneth/from-swing-to-jetpack-compose-desktop-2-4a4h)