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 File
s. 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)