Search code examples
androidkotlinandroid-jetpack-composeandroid-alertdialoglazycolumn

How can I update a LazyColumn's listitems inside an AlertDialog from a ViewModel properly in Jetpack Compose? - Android


I'm trying to make a recipe app for practice that saves Recipes and Ingredients in a ROOM database. The recipes are shown in the main screen, and there is a FloatingActionButton that lets you add a new recipe with ingredients. This FAB opens an AlertDialog where you can set the name and the description of the new recipe and also add ingredients. Initially there is only 1 AddIngredientRow (which is a composable function that I made for each ingredient that I want to add for the recipe). An Ingredient can have a 'count', 'unit' and a 'name', so there are 3 textfields on a single AddIngredientRow. Whenever you press 'Add ingredient' on the AlertDialog a new row will be inserted.

The problem is that I can't type anything into those TextFields. Whenever I type something nothing happens and if I add a new 'AddIngredientRow' the previously typed values will show up but only as a single character.

In my ViewModel I have an _addIngredients so I can modify the ingredients in the AlertDialog, and an addIngredients to get them as a List (I pass this list to the AlertDialog)

private val _addIngredients = mutableStateListOf<Ingredient>()
val addIngredients: List<Ingredient> = _addIngredients

I've already tried changing that, like passing a SnapshotStateList only, making it as a MutableStateFlow and other stuff. I use mutableStateListOf for now because this one updates the LazyColumn every time I add a new 'AddIngredientRow' (without the ingredient data).

This is how my AlertDialog works (I also tried changing LazyColumn's items block to itemsIndexed, and other ways to send the data to the 'AddIngredientRow'):

@Composable
fun AddRecipeDialog(
    state: RecipeState,
    addIngredients: List<Ingredient>,
    onEvent: (RecipeEvent) -> Unit
) {
    AlertDialog(
        onDismissRequest = {
            onEvent(RecipeEvent.HideDialog)
        },
        title = {
            Text(...)
        },
        text = {
            Column(...) {
                OutlinedTextField(...) // 'Recipe name'
                OutlinedTextField(...) // 'Recipe description'
                Text(...) // 'Add ingredients'
                LazyColumn() {
                    if (addIngredients.isEmpty()) {
                        onEvent(
                            RecipeEvent.AddIngredient(
                                Ingredient(
                                    name = "",
                                    count = 0,
                                    unit = ""
                                )
                            )
                        )
                    }
                    items(addIngredients) { ingredient ->
                        val index = addIngredients.indexOf(ingredient)
                        AddIngredientRow(
                            modifier = Modifier.padding(bottom = 20.dp),
                            onCountChange = { newValue ->
                                try {
                                    onEvent(RecipeEvent.SetIngredientCount(index, newValue.toInt()))
                                } catch (e: NumberFormatException) {
                                    e.printStackTrace()
                                }
                            },
                            onUnitChange = { newValue ->
                                onEvent(RecipeEvent.SetIngredientUnit(index, newValue))
                            },
                            onNameChange = { newValue ->
                                onEvent(RecipeEvent.SetIngredientName(index, newValue))
                            },
                            ingredient = ingredient
                        )
                    }
                }
            }
        },
        confirmButton = {
            Button(
                modifier = Modifier
                    .fillMaxWidth(),
                onClick = {
                    onEvent(RecipeEvent.AddRecipe)
                }
            ) {
                Text(
                    text = stringResource(id = R.string.save)
                )
            }
        },
        dismissButton = {
            Button(
                modifier = Modifier
                    .fillMaxWidth(),
                onClick = {
                    onEvent(
                        RecipeEvent.AddIngredient(
                            Ingredient(
                                name = "",
                                count = 0,
                                unit = ""
                            )
                        )
                    )
                }
            ) {
                Text(
                    text = stringResource(R.string.add_ingredient)
                )
            }
        }

    )
}

This is the AddIngredientRow:

@Composable
fun AddIngredientRow(
    ingredient: Ingredient,
    onCountChange: (String) -> Unit,
    onUnitChange: (String) -> Unit,
    onNameChange: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    Row(...) {
        OutlinedTextField(
            modifier = Modifier.weight(0.25f),
            value = ingredient.count.toString(),
            onValueChange = onCountChange,
            placeholder = {...},
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
        )
        OutlinedTextField(
            modifier = Modifier.weight(0.25f),
            value = ingredient.unit,
            onValueChange = onUnitChange,
            placeholder = {...}
        )
        OutlinedTextField(
            modifier = Modifier.weight(1f),
            value = ingredient.name,
            onValueChange = onNameChange,
            placeholder = {...}
        )
    }
}

This is how the ViewModel changes the ingredient values:

fun onEvent(event: RecipeEvent) {
        when(event) {
            // other events ...

            is RecipeEvent.SetIngredientCount -> {
                _addIngredients[event.index].count = event.count
            }
            is RecipeEvent.SetIngredientName -> {
                _addIngredients[event.index].name = event.name
            }
            is RecipeEvent.SetIngredientUnit -> {
                _addIngredients[event.index].unit = event.unit
            }
        }
    }

I'm guessing that the problem is the values I'm using in the TextFields are not really states. I don't know how to get the data properly from the ViewModel and also send the changes back.

I know this might be weird, probably would be better implemented in a new separate screen or something, but it's just for practice.


Solution

  • I found a solution. I made an IngredientState class that has the 3 values as MutableStates, so the UI actually can handle them as states. I don't know how optimal is this, but it's working now.

    In the viewmodel:

    val addIngredients = mutableStateListOf<IngredientState>()
    

    IngredientState:

    data class IngredientState(
        val name: MutableState<String>,
        val count: MutableState<String>,
        val unit: MutableState<String>
    )