Search code examples
androidandroid-jetpack-composekotlin-multiplatformcompose-desktopcompose-recomposition

Understanding Compose declarative logic


I'm new with Compose and declarative programming, and I'm trying to understand it. For learning, after reading tutorials and watching courses, now I'm creating my first app.

I'm creating a compose desktop application with compose multiplatform which will give you the possibility to select a folder from the computer and display all the files inside that folder. I'm launching a JFileChooser for selecting a folder. When it's selected, a state var is changed and a Box is filled with Texts representing the names of the files inside that folder. These names are obtained by a function which uses the path returned by JFileChooser.

The app has a two strange behaviours. First because that screen has a TextField and if I write inside it, the Box filled with texts seems to be repainted calling again the function which search for the files (and those can be thousands slowing the app).

The second strange behaviour is that if I open again the JFileChooser to change the folder, it repaints correctly the Box getting the file names of that folder, but if I select the same folder selected previously, the Box is not repainted, and if a file is changed in that folder, it is a problem.

I think both issues are related with declarative compose logic - what might be wrong in each case?

This is the button that displays the JFileChooser:

var listRomsState by remember { mutableStateOf(false) }
Button(onClick = {
    folderChosenPath = folderChooser()
    if (folderChosenPath != "")
        listRomsState = true
}) {
    Text(text = "List roms")
}

This is the function that shows the JFileChooser

fun folderChooser(): String {
    UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
    val f = JFileChooser()
    f.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY

    val result: Int = f.showSaveDialog(null)
    if (result == JFileChooser.APPROVE_OPTION) {
        return f.selectedFile.path
    } else {
        return ""
    }
}

Below the button that displays the file chooser is the list with the file names:

if (listRomsState) {
    RomsList(File(folderChosenPath))
}

This is the RomsList function:

@Composable
fun RomsList(folder: File) {
    Box (
        modifier = Modifier.fillMaxSize().border(1.dp, Color.LightGray)
    ) {
        LazyColumn(
            Modifier.fillMaxSize().padding(top = 5.dp, end = 8.dp)
        ){
            var romsList = getRomsFromFolder(folder)
            items(romsList.size) {
                Box (
                    modifier = Modifier.padding(5.dp, 0.dp, 5.dp, 0.dp).fillMaxWidth(),
                    contentAlignment = Alignment.CenterStart
                ) {
                    Row (horizontalArrangement = Arrangement.spacedBy(5.dp)){
                        Text(text = "" + (it+1), modifier = Modifier.weight(0.6f).background(color = Color(0, 0, 0, 20)))
                        Text(text = romsList[it].title, modifier = Modifier.weight(9.4f).background(color = Color(0, 0, 0, 20)))
                    }
                }
                Spacer(modifier = Modifier.height(5.dp))
            }
        }
    }
}

This is the function that recursively gets all the file names of a folder:

fun getRomsFromFolder(curDir: File? = File(".")): MutableList<Rom> {
    var romsList = mutableListOf<Rom>()

    val filesList = curDir?.listFiles()
    filesList?.let {
        for (f in filesList) {
            if (f.isDirectory) romsList.addAll(getRomsFromFolder(f))
            if (f.isFile) {
                romsList.add(Rom(f.name))
            }
        }
    }

    return romsList
}

Solution

  • Finally I discovered some things with the help of Steyrix, z.y and some other guys.

    First, as noted here, https://developer.android.com/jetpack/compose/mental-model#recomposition composable functions only execute their code if their parameters have been changed, so that is the cause of issue 2.

    Also, the main problem of both issues is that I was executing the logic that retrieves all the files inside a folder in a wrong place. It was being executed in a composable function, and that's a problem, because it will be executed each time is recomposed. So moving the logic to the onclick, just after the result of the file chooser has been received solved both issues.

    Also, now I understand much more things.