androidkotlinandroid-jetpack-composeviewmodelandroid-mvvm

multiple instances with compose and kotlin


sorry for the English, I'm using a translator.i'm just a beginner, please consider this when replying.i know the code may not be the best. this is just a start. I'm using the viewmodel to create a template and then use this template in several instances,the problem is that when you enter a value in the field for item 1, the other field is also filled at the same time. I'd like to know the error, and why the code isn't working even though I've created different instances.

main file:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MercadoTheme {
                Aplicacao()
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Aplicacao(modifier: Modifier = Modifier) {
    val item1VM: ItemInputViewModel = viewModel()
    val item2VM: ItemInputViewModel = viewModel()
    val item1Layout = ItemInput()
    val item1State by item1VM.itemState.collectAsState()

    val item2Layout = ItemInput()
    val item2State by item2VM.itemState.collectAsState()
    
    Scaffold(modifier.fillMaxSize(),
        topBar = {
            TopAppBar(
                modifier = modifier.fillMaxWidth(),
                title = {
                    Text(
                        modifier = modifier.fillMaxWidth(),
                        text = "Comparador de Preços",
                        color = CompareTopAppBarTextColor,
                        fontWeight = FontWeight.Bold,
                        fontSize = 25.sp,
                        textAlign = TextAlign.Center,
                    )
                },
                colors = TopAppBarDefaults.topAppBarColors(CompareTopAppBarBackgroundColor)
            )
        },
        content = { innerPadding ->
            Column(modifier = Modifier.padding(innerPadding)) {
               item1Layout.Insert(
                    itemData = item1State,
                    onValorChange = { item1State.onValorChange(it) },
                    onQuantidadeChange = { item1State.onQuantidadeChange(it) },
                    onUnidadeChange = { item1State.onUnidadeChange(it) },
                    onExpandedChange = { item1State.onExpandedChange(it) }
                )
                //---------------
                Spacer(modifier = Modifier.size(20.dp))
                item2Layout.Insert(
                    itemData = item2State,
                    onValorChange = { item2State.onValorChange(it) },
                    onQuantidadeChange = { item2State.onQuantidadeChange(it) },
                    onUnidadeChange = { item2State.onUnidadeChange(it) },
                    onExpandedChange = { item2State.onExpandedChange(it) }
                )
            }
        })
}

UI file:

class ItemInput {
    @Composable
    fun Insert(
        modifier: Modifier = Modifier,
        itemData: ItemData,
        onValorChange: (String) -> Unit = {},
        onQuantidadeChange: (String) -> Unit = {},
        onUnidadeChange: (String) -> Unit = {},
        onExpandedChange: (Boolean) -> Unit = {}
    ) {
        var textFieldSize by remember { mutableStateOf(Size.Zero) }
        val unidadesOptions = listOf("-", "Kg", "g", "L", "mL", "m", "Cm", "mm")

        Column {
            Text(
                modifier = modifier
                    .padding(10.dp)
                    .align(Alignment.CenterHorizontally),
                text = itemData.name,
                style = MaterialTheme.typography.titleLarge
            )
            Row(
                modifier = modifier.padding(10.dp),
                content = {
                    // Valor
                    OutlinedTextField(
                        modifier = Modifier.weight(3f),
                        singleLine = true,
                        keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
                        label = { Text("Valor") },
                        value = itemData.valor,
                        onValueChange = onValorChange
                    )

                    Spacer(modifier = Modifier.width(8.dp))

                    // Quantidade
                    OutlinedTextField(
                        modifier = Modifier.weight(3f),
                        singleLine = true,
                        keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
                        label = { Text("Quantidade") },
                        value = itemData.quantidade,
                        onValueChange = onQuantidadeChange
                    )

                    Spacer(modifier = Modifier.width(8.dp))

                    // Unidade
                    OutlinedTextField(
                        readOnly = true,
                        value = itemData.unidade,
                        onValueChange = {},
                        label = { Text("Unidade") },
                        trailingIcon = {
                            Icon(
                                imageVector = Icons.Default.ArrowDropDown,
                                contentDescription = "Open dropdown",
                                modifier = Modifier.size(24.dp)
                            )
                        },
                        modifier = Modifier
                            .weight(2f)
                            .onGloballyPositioned { coordinates ->
                                textFieldSize = coordinates.size.toSize()
                            }
                            .clickable { onExpandedChange(true) }
                    )

                    // Menu
                    DropdownMenu(
                        expanded = itemData.expanded,
                        onDismissRequest = { onExpandedChange(false) },
                        modifier = Modifier
                            .width(textFieldSize.width.dp)
                            .padding(top = 8.dp)
                    ) {
                        unidadesOptions.forEach { item ->
                            DropdownMenuItem(
                                text = { Text(item) },
                                onClick = {
                                    onUnidadeChange(item)
                                    onExpandedChange(false)
                                }
                            )
                        }
                    }
                }
            )
        }
    }
}

VM file:

data class ItemData(
    val name: String = "Item",
    val valor: String = "",
    val quantidade: String = "",
    val unidade: String = "-",
    val expanded: Boolean = false,
    val onNameChange: (String) -> Unit = {},
    val onValorChange: (String) -> Unit = {},
    val onQuantidadeChange: (String) -> Unit = {},
    val onUnidadeChange: (String) -> Unit = {},
    val onExpandedChange: (Boolean) -> Unit = {}
)

class ItemInputViewModel : ViewModel() {
    private val _itemState = MutableStateFlow(ItemData())
    val itemState = _itemState.asStateFlow()

    init {
        _itemState.update { currentState ->
            currentState.copy(
                onNameChange = { newValue ->
                    _itemState.value = _itemState.value.copy(name = newValue)
                },
                onValorChange = { newValue ->
                    _itemState.value = _itemState.value.copy(valor = newValue)
                },
                onQuantidadeChange = { newValue ->
                    _itemState.value = _itemState.value.copy(quantidade = newValue)
                },
                onUnidadeChange = { newValue ->
                    _itemState.value = _itemState.value.copy(unidade = newValue)
                },
                onExpandedChange = { newValue ->
                    _itemState.value = _itemState.value.copy(expanded = newValue)
                }
            )
        }
    }
}

I've tried passing the parameters like this, but the same error still occurs

fun Aplicacao(modifier: Modifier = Modifier,
              item1VM: ItemInputViewModel = viewModel(),
              item2VM: ItemInputViewModel = viewModel()
    )

Solution

  • When you use viewModel(), that is putting a ViewModel instance in a ViewModelStoreOwner - in your case, your activity.

    Every time you call viewModel() for a particular class, you'll get the same instance back - that's how you get the same instance back after a configuration change, etc.

    That means that when you write

    val item1VM: ItemInputViewModel = viewModel()
    val item2VM: ItemInputViewModel = viewModel()
    

    item1VM and item2VM are the exact same instance - the first call creates an instance of ItemInputViewModel and the second just retrieves that single instance.

    If you want to create multiple instances, then you should use the key parameter of viewModel, as explained in the documentation:

    The key to use to identify the ViewModel.

    val item1VM: ItemInputViewModel = viewModel("item1")
    val item2VM: ItemInputViewModel = viewModel("item2")