You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin Compose多ViewModel实例数据同步异常问题排查求助

问题描述

我是Kotlin Compose初学者,用翻译工具表述问题,见谅。尝试通过ItemInputViewModel创建模板并生成多个实例,但输入Item1的字段值时,Item2的对应字段会同步填充。调整参数传递方式后问题仍存在,附上代码求排查原因。

主文件

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文件

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文件

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)
                }
            )
        }
    }
}

已尝试的参数传递方式

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

问题原因与修复方案

核心问题

  1. ViewModel实例复用:Compose的viewModel()函数默认会基于当前Activity复用同一个ViewModel实例,你创建的item1VM和item2VM其实是同一个对象,导致状态同步变化。
  2. 数据类设计反模式:把回调函数存在ItemData中,不仅会导致逻辑混乱,还容易引发意外的引用持有问题。

修复步骤

1. 给ViewModel指定唯一标识,创建独立实例

调用viewModel()时传入key参数,确保每个ViewModel都是独立的:

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

2. 重构ItemData,剥离回调函数

数据类只存储纯状态数据,移除所有回调:

data class ItemData(
    val name: String = "Item",
    val valor: String = "",
    val quantidade: String = "",
    val unidade: String = "-",
    val expanded: Boolean = false
)

3. 调整ViewModel的状态更新逻辑

在ViewModel中直接暴露更新方法,替代原有的回调逻辑:

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

    fun updateName(newName: String) {
        _itemState.update { it.copy(name = newName) }
    }

    fun updateValor(newValor: String) {
        _itemState.update { it.copy(valor = newValor) }
    }

    fun updateQuantidade(newQuantidade: String) {
        _itemState.update { it.copy(quantidade = newQuantidade) }
    }

    fun updateUnidade(newUnidade: String) {
        _itemState.update { it.copy(unidade = newUnidade) }
    }

    fun updateExpanded(newExpanded: Boolean) {
        _itemState.update { it.copy(expanded = newExpanded) }
    }
}

4. 修改UI调用逻辑

直接调用ViewModel的更新方法,不再依赖ItemData中的回调:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Aplicacao(modifier: Modifier = Modifier) {
    val item1VM: ItemInputViewModel = viewModel(key = "item1")
    val item2VM: ItemInputViewModel = viewModel(key = "item2")
    val item1State by item1VM.itemState.collectAsState()
    val item2State by item2VM.itemState.collectAsState()
    
    Scaffold(modifier.fillMaxSize(),
        topBar = { /* 保留原有TopAppBar代码 */ },
        content = { innerPadding ->
            Column(modifier = Modifier.padding(innerPadding)) {
                ItemInputInsert(
                    itemData = item1State,
                    onValorChange = item1VM::updateValor,
                    onQuantidadeChange = item1VM::updateQuantidade,
                    onUnidadeChange = item1VM::updateUnidade,
                    onExpandedChange = item1VM::updateExpanded
                )
                Spacer(modifier = Modifier.size(20.dp))
                ItemInputInsert(
                    itemData = item2State,
                    onValorChange = item2VM::updateValor,
                    onQuantidadeChange = item2VM::updateQuantidade,
                    onUnidadeChange = item2VM::updateUnidade,
                    onExpandedChange = item2VM::updateExpanded
                )
            }
        })
}

5. 重构UI组件为顶层Composable

去掉ItemInput类封装,直接用顶层Composable函数,符合Compose设计习惯:

@Composable
fun ItemInputInsert(
    modifier: Modifier = Modifier,
    itemData: ItemData,
    onValorChange: (String) -> Unit = {},
    onQuantidadeChange: (String) -> Unit = {},
    onUnidadeChange: (String) -> Unit = {},
    onExpandedChange: (Boolean) -> Unit = {}
) {
    // 保留原有UI实现代码不变
}

额外建议

  • 避免用类封装Composable函数,直接写顶层/扩展函数更贴合Compose理念。
  • 状态更新逻辑统一放在ViewModel中,UI只负责展示和传递用户操作。
  • 多实例ViewModel必须通过key参数区分,防止复用同一实例。

内容的提问来源于stack exchange,提问作者Ramon Xavier

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 15:34:49