androidkotlinandroid-jetpack-composeandroid-jetpack-compose-material3

Exposed drop down menu won't recompose until I close the drop down menu


I have a statistics screen that uses drop down menus to add filters. One filter for example has all of the game types with check boxes next to each game. It's initially populated with all of the boxes checked so all games are included in the stats. As the user unchecks games the stats update without the games that were unchecked. Everything is working as I want except that the statistics won't show the updated values until I close the dropdown menu. The checkboxes are updating as they are checked. I used debug to see that the statistics are updated as the boxes are checked or unchecked.


val selectedGame = remember { SnapshotStateList<String>() }

ExposedDropdownMenuBox(
    expanded = expandedGame,
    onExpandedChange = { onExpandGameChange(it) },
) {

    ShowOutlinedTextField(
        readOnly = true,
        value = stringResource(id = R.string.game_filter),
        modifier = Modifier
            .padding(10.dp)
            .menuAnchor(type = MenuAnchorType.PrimaryNotEditable, 
             enabled = true)
    )

    ExposedDropdownMenu(
        expanded = expandedGame,
        onDismissRequest = { onExpandGameChange(false) }
    ) {
        gameNames.forEach { game ->
            DropdownMenuItem(
                onClick = { onExpandGameChange(false) },
                text = {
                    Row(
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Checkbox(
                            checked = 
                        selectedGame.contains(game.gameName),
                            onCheckedChange = { isChecked ->
                                if (isChecked) {
                                    selectedGame.add(game.gameName)
                                } else {
                                    selectedGame.remove(game.gameName)
                                }
                                
                         setSelectedGames(selectedGame.toList())
                            }
                        )
                        Spacer(modifier = Modifier.width(8.dp))
                        Text(text = game.gameName)
                    }
                }
            )
        }
    }
}

I tried adding the following code but it didn't work.

var forceRecomposition by remember { mutableStateOf(0) }

Checkbox(
    checked = selectedLocations.contains(casino.name),
    onCheckedChange = { isChecked ->
        if (isChecked) {
            selectedLocations.add(casino.name)
        } else {
            selectedLocations.remove(casino.name)
        }
        setSelectedCasinos(selectedLocations.toList())
        forceRecomposition++
    }
)

Solution

  • You should lift the state that is common to the ExposedDropdownMenu Composable and your statistics Composable to the common parent Composable. Then, when the user clicks a Checkbox,

    Please have a look at the following sample code:

    @Composable
    fun MyComposableScreen() {
        val items = remember { arrayOf("Item A", "Item B", "Item C", "Item D") }
        val selectedItems = remember { mutableStateListOf(*items) }
    
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.SpaceAround,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            MyDropdownMenu(
                allItems = items.toList(),
                selectedItems = selectedItems,
                addItem = { newItem ->
                    selectedItems.add(newItem)
                },
                removeItem = { removedItem ->
                    selectedItems.remove(removedItem)
                }
            )
            MyStatistics(selectedItems)
        }
    }
    

    The MyDropdownMenu Composable looks as follows:

    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun MyDropdownMenu(
        allItems: List<String>,
        selectedItems: List<String>,
        addItem: (String) -> Unit,
        removeItem: (String) -> Unit
    ) {
    
        var isExpanded by remember { mutableStateOf(false) }
    
        ExposedDropdownMenuBox(
            expanded = isExpanded,
            onExpandedChange = { isExpanded = it },
        ) {
    
            TextField(
                modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable),
                state = TextFieldState(selectedItems.joinToString(",")),
                readOnly = true,
                lineLimits = TextFieldLineLimits.SingleLine,
                label = { Text("Label") },
                trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
                colors = ExposedDropdownMenuDefaults.textFieldColors(),
            )
    
            ExposedDropdownMenu(
                expanded = isExpanded,
                onDismissRequest = { isExpanded = false }
            ) {
                allItems.forEach { itemName: String ->
                    DropdownMenuItem(
                        onClick = { /** do nothing or call addItem() / removeItem() **/ },
                        text = {
                            Row(
                                verticalAlignment = Alignment.CenterVertically
                            ) {
                                Checkbox(
                                    checked = selectedItems.contains(itemName),
                                    onCheckedChange = { isChecked ->
                                        if (isChecked) {
                                            addItem(itemName)
                                        } else {
                                            removeItem(itemName)
                                        }
                                    }
                                )
                                Spacer(modifier = Modifier.width(8.dp))
                                Text(text = itemName)
                            }
                        }
                    )
                }
            }
        }
    }
    

    Finally, the MyStatistics Composable could look like this:

    @Composable
    fun MyStatistics(selectedItems: List<String>) {
        Text(
            "SELECTED ITEMS: \n${selectedItems.joinToString(",")}",
            fontSize = 24.sp
        )
    }
    

    Note that is good practice to use mutableStateListOf instead of directly remembering a SnapshotStateList.

    Output:

    Screen Recording