I have a mutableStateListOf(ReturnItemExamples) and each of the objects in the list are being displayed in a LazyColumn.
The thing I am having trouble with is once I update the quantity using the OutlinedTextField, and it calls the API (on the viewModel) to update the record, after the API returns successful results and updates those to the mutableStateListOf(ReturnItemExamples) list, the update to the quantity, isn't being shown in the OutlinedTextField. I believe that the update is happening and that it is triggering a redraw by the composable, because I have other fields that are being updated, the only field I have trouble is the OutlinedTextField. That is the only one that isn't updating on screen.
Here is the data model that has the quantity property:
class ReturnItemExample (
val itemCode: String? = null,
val itemDescription: String? = null,
var quantity: Int = 0
)
Here's the 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 {
// Call to an API here returns the updated ReturnItemExample object and I update it here
// with a function that sets the item from the API response to the item in the array at
// the specified index
returnItems[index] = API.response
}
}
}
Here is the listview:
@Composable
fun ReturnsListViewExample(
viewModel: ReturnsViewModelExample,
) {
val itemIndex = remember { mutableIntStateOf(0) }
Column {
Box() {
LazyColumn(state = rememberLazyListState()) {
itemsIndexed(viewModel.returnItems) { index, _ ->
ReturnItemViewExample(
viewModel = viewModel,
itemIndex = index
)
}
}
}
}
}
Here is the composable that show the OutlinedTextField I am having trouble with:
@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)
)
}
}
Have a look at the following line in your code:
var testDisplayQuantity by remember { mutableStateOf(itemData.quantity.toString()) }
It creates a new state variable of type String and initializes it to itemData.quantity.toString()
. After this initialization, there is no more connection to the original itemData object. It is completely independent.
I recommend two steps to resolve this problem:
itemData
, it will have no effect. Jetpack Compose cannot detect when you modify one property of a state object. Instead, it can only recompose once a reference changed.testDisplayQuantity
state variable or you can introduce logic that refreshes the testDisplayQuantity
variable whenever the itemData
changes.Step 1)
In order to update the quantity of an item, create a ViewModel function similar to this:
fun updateQuantity(index: Int, quantity: String) {
returnItems.set(index, returnItems[index].copy(quantity = quantity.toInt()))
}
We replace an instance in the list on position index with the set
function and assign a new instance with updated quantity using the copy
function. Jetpack Compose detects the change and will recompose the LazyList.
Then call it in onValueChange
like this:
onValueChange = {
viewModel.updateQuantity(itemIndex, it)
}
Step 2)
Replace all occurences of testDisplayQuantity
with itemData.quantity.toString()
and delete the variable.
Alternatively, you can use a LaunchedEffect
to update your testDisplayQuantity
variable whenever the itemData
changes:
val itemData = viewModel.returnItems[itemIndex]
var testDisplayQuantity by remember { mutableStateOf(itemData.quantity.toString()) }
LaunchedEffect(itemData) {
testDisplayQuantity = itemData.quantity.toString()
}
As a side note, please consider to pass the item itself to the ReturnItemViewExample
Composable instead of the index in the list. It results in cleaner code and makes your local itemData
state variable redundant.
items(viewModel.returnItems) { item ->
ReturnItemViewExample(
viewModel = viewModel,
itemData = item
)
}
Then update your Composable signature as follows:
fun ReturnItemViewExample(
viewModel: ReturnsViewModelExample,
itemData: ReturnItemExample
) {
//...
}
Also, you should update your ReturnItemExample
class as follows:
data class ReturnItemExample (
val itemCode: String? = null,
val itemDescription: String? = null,
val quantity: Int = 0
)
When you have a var inside of a data class, this will usually result in things not working in Jetpack Compose.