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.clear();
df.scanDir(textfieldBasedir.getText(), true);
df.removeSingles();
checksums = df.getChecksums();
updateGUI();
}
df and checksums are simple member variables.
private TKDupeFinder df = new TKDupeFinder();
private String[] checksums = {};
The Swing code for updateGUI() looks lie this:
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:
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) {
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:
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:
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.
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.
@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:
@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:
@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:
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


Top comments (0)