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

