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

Jetpack Compose中ViewModel更新后OutlinedTextField不刷新问题

问题描述

使用mutableStateListOf<ReturnItemExample>存储数据并在LazyColumn展示列表项,修改OutlinedTextField的quantity后调用ViewModel中的API更新记录,API返回成功并更新了mutableStateListOf中的数据,但OutlinedTextField并未显示更新后的quantity值。其他字段能正常刷新,说明Composable已触发重绘,仅该输入框存在问题。

数据模型

class ReturnItemExample (
    val itemCode: String? = null,
    val itemDescription: String? = null,
    var quantity: Int = 0
)

ViewModel代码

@HiltViewModel
class ReturnsViewModelExample @Inject constructor(
    val app: Application,
    val authToken: String,
    private val repository: ReturnsRepository,
) : AndroidViewModel(app) {

    val returnItems = mutableStateListOf<ReturnItemExample>()

    private fun updateReturnExample(quantity: Int, index: Int) {
        viewModelScope.launch {
            // 调用API获取更新后的ReturnItemExample对象,替换列表对应索引的项
            returnItems[index] = API.response
        }
    }
}

列表Composable

@Composable
fun ReturnsListViewExample(
    viewModel: ReturnsViewModelExample,
) {
    val itemIndex = remember { mutableIntStateOf(0) }

    Column {
        Box() {
            LazyColumn(state = rememberLazyListState()) {
                itemsIndexed(viewModel.returnItems) { index, _ ->
                    ReturnItemViewExample(
                        viewModel = viewModel,
                        itemIndex = index
                    )
                }
            }
        }
    }
}

问题相关的Item Composable

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ReturnItemViewExample(
    viewModel: ReturnsViewModelExample,
    itemIndex: Int
) {
    val keyboardController = LocalSoftwareKeyboardController.current
    val itemData = viewModel.returnItems[itemIndex]
    var testDisplayQuantity by remember { mutableStateOf(itemData.quantity.toString()) }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp)
            .padding(bottom = 8.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = "Test Quantity",
            fontSize = 14.sp,
            fontWeight = FontWeight.SemiBold,
            textAlign = TextAlign.Center
        )

        OutlinedTextField(
            value = testDisplayQuantity,
            onValueChange = { testDisplayQuantity = it },
            textStyle = LocalTextStyle.current.copy(fontSize = 14.sp),
            maxLines = 1,
            modifier = Modifier
                .fillMaxWidth(.5f)
                .padding(horizontal = 8.dp)
                .onKeyEvent {
                    if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) {
                        if (validQuantity(testDisplayQuantity.toInt())) {
                            viewModel.updateReturnExample(testDisplayQuantity.toInt(), itemIndex)
                        }
                    }
                    true
                },
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Number,
                imeAction = ImeAction.Done
            ),
            keyboardActions = KeyboardActions(
                onDone = {
                    keyboardController?.hide()
                    if (validQuantity(testDisplayQuantity.toInt())) {
                        viewModel.updateReturnExample(testDisplayQuantity.toInt(), itemIndex)
                    }
                }),
            shape = RoundedCornerShape(10.dp)
        )
    }
}

原因分析

核心问题在于testDisplayQuantity是通过remember创建的本地状态,仅在Composable首次组合时从itemData.quantity初始化一次。当ViewModel中的returnItems更新后,尽管Composable触发重绘,但remember会保留之前的本地状态值,不会自动同步ViewModel中最新的itemData.quantity。

另外,数据模型ReturnItemExample中的quantity是var,直接修改属性无法触发mutableStateListOf的变更检测(它仅检测列表元素的替换,不检测元素内部属性的修改),不过你的ViewModel代码中是替换整个对象,这部分是正确的,问题主要出在本地状态的同步上。


解决方案

方案1:用derivedStateOf同步ViewModel数据

将本地状态替换为derivedStateOf,它会自动监听itemData.quantity的变化并更新输入框显示值,同时处理输入时的双向同步:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ReturnItemViewExample(
    viewModel: ReturnsViewModelExample,
    itemIndex: Int
) {
    val keyboardController = LocalSoftwareKeyboardController.current
    val itemData = viewModel.returnItems[itemIndex]
    
    // 从ViewModel数据派生状态,自动同步更新
    val testDisplayQuantity by remember {
        derivedStateOf { itemData.quantity.toString() }
    }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp)
            .padding(bottom = 8.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = "Test Quantity",
            fontSize = 14.sp,
            fontWeight = FontWeight.SemiBold,
            textAlign = TextAlign.Center
        )

        OutlinedTextField(
            value = testDisplayQuantity,
            onValueChange = { input ->
                // 输入时实时更新ViewModel数据,保证双向同步
                runCatching { input.toInt() }.onSuccess { quantity ->
                    viewModel.returnItems[itemIndex] = itemData.copy(quantity = quantity)
                }
            },
            textStyle = LocalTextStyle.current.copy(fontSize = 14.sp),
            maxLines = 1,
            modifier = Modifier
                .fillMaxWidth(.5f)
                .padding(horizontal = 8.dp)
                .onKeyEvent {
                    if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) {
                        runCatching { testDisplayQuantity.toInt() }.onSuccess { quantity ->
                            if (validQuantity(quantity)) {
                                viewModel.updateReturnExample(quantity, itemIndex)
                            }
                        }
                        keyboardController?.hide()
                    }
                    true
                },
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Number,
                imeAction = ImeAction.Done
            ),
            keyboardActions = KeyboardActions(
                onDone = {
                    keyboardController?.hide()
                    runCatching { testDisplayQuantity.toInt() }.onSuccess { quantity ->
                        if (validQuantity(quantity)) {
                            viewModel.updateReturnExample(quantity, itemIndex)
                        }
                    }
                }),
            shape = RoundedCornerShape(10.dp)
        )
    }
}

方案2:直接绑定ViewModel数据,移除本地状态

取消本地testDisplayQuantity状态,直接将输入框值绑定到ViewModel中的itemData.quantity,简化状态管理:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ReturnItemViewExample(
    viewModel: ReturnsViewModelExample,
    itemIndex: Int
) {
    val keyboardController = LocalSoftwareKeyboardController.current
    val itemData = viewModel.returnItems[itemIndex]

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp)
            .padding(bottom = 8.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = "Test Quantity",
            fontSize = 14.sp,
            fontWeight = FontWeight.SemiBold,
            textAlign = TextAlign.Center
        )

        OutlinedTextField(
            value = itemData.quantity.toString(),
            onValueChange = { input ->
                runCatching { input.toInt() }.onSuccess { quantity ->
                    viewModel.returnItems[itemIndex] = itemData.copy(quantity = quantity)
                }
            },
            textStyle = LocalTextStyle.current.copy(fontSize = 14.sp),
            maxLines = 1,
            modifier = Modifier
                .fillMaxWidth(.5f)
                .padding(horizontal = 8.dp)
                .onKeyEvent {
                    if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) {
                        runCatching { itemData.quantity.toString().toInt() }.onSuccess { quantity ->
                            if (validQuantity(quantity)) {
                                viewModel.updateReturnExample(quantity, itemIndex)
                            }
                        }
                        keyboardController?.hide()
                    }
                    true
                },
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Number,
                imeAction = ImeAction.Done
            ),
            keyboardActions = KeyboardActions(
                onDone = {
                    keyboardController?.hide()
                    runCatching { itemData.quantity.toString().toInt() }.onSuccess { quantity ->
                        if (validQuantity(quantity)) {
                            viewModel.updateReturnExample(quantity, itemIndex)
                        }
                    }
                }),
            shape = RoundedCornerShape(10.dp)
        )
    }
}

额外优化:修改数据模型为不可变

将ReturnItemExample的quantity改为val,使用copy方法创建新对象,符合不可变数据原则,也能确保mutableStateListOf正确检测到元素变更:

class ReturnItemExample (
    val itemCode: String? = null,
    val itemDescription: String? = null,
    val quantity: Int = 0 // 改为val
)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 13:17:47