Reputation: 1396
I am working with an edit screen of a Room database app. After getting a single record and populating the TextFields with the values, I can't edit the TextFields anymore. It looks like the data is getting overwritten in every keystroke. How can I fix this?
val receivedItem = itemViewModel.getItem(itemId!!.toInt()).observeAsState()
val item = receivedItem.value ?: Item(0, "", 0.0, 0)
// if I assign the values here from the database, I get all empty values
var itemName by remember { mutableStateOf("") }
var itemPrice by remember { mutableStateOf("") }
var itemQuantity by remember { mutableStateOf("") }
// if I wrap these values in LaunchEffect(Unit), they don't get assigned at all
itemName = item.itemName
itemPrice = item.itemPrice.toString()
itemQuantity = item.quantityInStock.toString()
Column(
modifier = Modifier
.padding(horizontal = 32.dp, vertical = 16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
OutlinedTextField(
value = itemName,
onValueChange = { itemName = it },
label = { Text("Item name") },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
OutlinedTextField(
value = itemPrice,
onValueChange = { itemPrice = it },
label = { Text("Item price") },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
OutlinedTextField(
value = itemQuantity,
onValueChange = { itemQuantity = it },
label = { Text("Item quantity") },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
...
Thanks for your help!
Upvotes: 0
Views: 1553
Reputation: 1396
Found the bug and fixed the issue myself.
The issue was with this line:
val receivedItem = itemViewModel.getItem(itemId!!.toInt()).observeAsState()
The receivedItem
initially gets an empty value from the view model and gets updated after at least a couple of recompositions, like this:
2021-08-17 14:48:52.221 7985-7985/com.example.roomcrud D/EditScreen: Item id: 3
2021-08-17 14:48:52.221 7985-7985/com.example.roomcrud D/EditScreen: receivedItem: Item(id=0, itemName=, itemPrice=0.0, quantityInStock=0)
2021-08-17 14:48:52.221 7985-7985/com.example.roomcrud D/EditScreen: Editing values: itemName - , itemPrice - 0.0, itemQuantity - 0
2021-08-17 14:48:52.255 7985-7985/com.example.roomcrud D/EditScreen: Item id: 3
2021-08-17 14:48:52.255 7985-7985/com.example.roomcrud D/EditScreen: receivedItem: Item(id=3, itemName=Mango, itemPrice=2.2, quantityInStock=27)
2021-08-17 14:48:52.255 7985-7985/com.example.roomcrud D/EditScreen: Editing values: itemName - , itemPrice - 0.0, itemQuantity - 0
2021-08-17 14:48:52.271 7985-7985/com.example.roomcrud D/EditScreen: Item id: 3
2021-08-17 14:48:52.271 7985-7985/com.example.roomcrud D/EditScreen: receivedItem: Item(id=3, itemName=Mango, itemPrice=2.2, quantityInStock=27)
2021-08-17 14:48:52.271 7985-7985/com.example.roomcrud D/EditScreen: Editing values: itemName - Mango, itemPrice - 2.2, itemQuantity - 27
2021-08-17 14:48:52.553 7985-7985/com.example.roomcrud D/EditScreen: Item id: 3
2021-08-17 14:48:52.554 7985-7985/com.example.roomcrud D/EditScreen: receivedItem: Item(id=3, itemName=Mango, itemPrice=2.2, quantityInStock=27)
2021-08-17 14:48:52.554 7985-7985/com.example.roomcrud D/EditScreen: Editing values: itemName - Mango, itemPrice - 2.2, itemQuantity - 27
2021-08-17 14:48:52.568 7985-7985/com.example.roomcrud D/EditScreen: Item id: 3
2021-08-17 14:48:52.569 7985-7985/com.example.roomcrud D/EditScreen: receivedItem: Item(id=3, itemName=Mango, itemPrice=2.2, quantityInStock=27)
2021-08-17 14:48:52.569 7985-7985/com.example.roomcrud D/EditScreen: Editing values: itemName - Mango, itemPrice - 2.2, itemQuantity - 27
So when I was remembering the values, they were empty. If I then tried to update the values with local variables, that got overwritten on every recomposition. So I've minimized the overwriting by using LaunchedEffect
and updating the values only once when the actual value from the database arrived and was not empty.
I can now edit the values on the TextFields alright, as they are not getting overwritten over and over again on every recomposition.
Here is the updated code that finally worked:
@Composable
fun EditScreen(
itemId: String?,
navController: NavController,
itemViewModel: ItemViewModel,
onSetAppTitle: (String) -> Unit,
onShowFab: (Boolean) -> Unit
) {
LaunchedEffect(Unit) {
onSetAppTitle("Edit Item")
onShowFab(false)
}
Log.d("EditScreen", "Item id: $itemId")
val receivedItem: Item by itemViewModel.getItem(itemId!!.toInt())
.observeAsState(Item(0, "", 0.0, 0))
Log.d("EditScreen", "receivedItem: $receivedItem")
var itemName by remember { mutableStateOf(receivedItem.itemName) }
var itemPrice by remember { mutableStateOf(receivedItem.itemPrice.toString()) }
var itemQuantity by remember { mutableStateOf(receivedItem.quantityInStock.toString()) }
if (receivedItem.id != 0) {
LaunchedEffect(Unit) {
itemName = receivedItem.itemName
itemPrice = receivedItem.itemPrice.toString()
itemQuantity = receivedItem.quantityInStock.toString()
}
}
Log.d(
"EditScreen",
"Editing values: itemName - $itemName, itemPrice - $itemPrice, itemQuantity - $itemQuantity"
)
Column(
modifier = Modifier
.padding(horizontal = 32.dp, vertical = 16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
OutlinedTextField(
value = itemName,
onValueChange = { itemName = it },
label = { Text("Item name") },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
OutlinedTextField(
value = itemPrice,
onValueChange = { itemPrice = it },
label = { Text("Item price") },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
OutlinedTextField(
value = itemQuantity,
onValueChange = { itemQuantity = it },
label = { Text("Item quantity") },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
Button(
onClick = {
if (itemViewModel.isItemValid(itemName, itemPrice, itemQuantity)) {
var updatedItem = receivedItem.copy(
itemName = itemName.trim(),
itemPrice = itemPrice.trim().toDouble(),
quantityInStock = itemQuantity.trim().toInt()
)
itemViewModel.updateItem(updatedItem)
navController.navigate("home") {
popUpTo("home") { inclusive = true }
}
}
}, modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
) {
Text(text = "Save")
}
}
}
Hope it helps someone in the future! Thanks, everyone for your answers!
Upvotes: 2
Reputation: 6835
You are reading the values live from the viewmodel, while calling observeAsState
. There are fixes alright. You can either do something like
val receivedItem = itemViewModel.getItem(itemId!!.toInt()).observeAsState()
val item = receivedItem.value ?: Item(0, "", 0.0, 0)
var itemName by remember { mutableStateOf(item.itemName) }
var itemPrice by remember { mutableStateOf(item.itemPrice.toString()) }
var itemQuantity by remember { mutableStateOf(item.quantityInStock.toString) }
//Why these?
/*
itemName = item.itemName
itemPrice = item.itemPrice.toString()
itemQuantity = item.quantityInStock.toString()
*/
Column(
modifier = Modifier
.padding(horizontal = 32.dp, vertical = 16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
OutlinedTextField(
value = itemName,
onValueChange = { itemName = it },
label = { Text("Item name") },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
OutlinedTextField(
value = itemPrice,
onValueChange = { itemPrice = it },
label = { Text("Item price") },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
OutlinedTextField(
value = itemQuantity,
onValueChange = { itemQuantity = it },
label = { Text("Item quantity") },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
...
Please note that this does NOT modify the values inside the viewmodel. The correct approach would be to create a parameter in this composable called onItemChange, and then hook it up to the viewmodel's onItemChange method, which would directly modify it inside the viewmodel. Check out state hoisting
Upvotes: 0
Reputation: 187
The problem that is occurring is that every time you change the value of itemName, itemPrice and itemQuantity, the composable recreates itself to display the data. This forces the lines of code
itemName = item.itemName
itemPrice = item.itemPrice.toString()
itemQuantity = item.quantityInStock.toString()
to be run on every recomposition, thus resetting your text fields. An easy fix would be to simply have the initial mutable state values to be defined with the item values from the get go like so, with the above 3 lines removed. This way the text value will only be overridden when a new value receivedItem from the viewmodel will be observed.
var itemName by remember { mutableStateOf(item.itemName) }
var itemPrice by remember { mutableStateOf(item.itemPrice) }
var itemQuantity by remember { mutableStateOf(item.itemQuantity) }
Upvotes: 0