Here is MainViewModel.kt:
class MainViewModel : ViewModel() {
var fruit by mutableStateOf("") // changed only by changeFruit() method
private set
/** Validates and changes [fruit] and returns the result. */
fun changeFruit(value: String): String {
fruit = if (value == "apple") "banana" else value
return fruit
}
// ...other methods that can change the fruit variable with changeFruit()...
}
Here is MainActivity.kt:
import ...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Tests5Theme {
App()
}
}
}
}
@Composable
fun App() {
val vm: MainViewModel = viewModel()
Column {
var fieldValue by remember { mutableStateOf(vm.fruit) }
Field(
value = fieldValue,
onValueChange = { fieldValue = it },
onDone = { vm.changeFruit(fieldValue) }
)
// I know onClick could be { fieldValue = vm.changeFruit("apple") }
// and { fieldValue = vm.changeFruit("strawberry") }
// but what if these buttons are placed away in other composables?
Button(onClick = { vm.changeFruit("apple") }) {
Text(text = "Change fruit to apple")
}
Button(onClick = { vm.changeFruit("strawberry") }) {
Text(text = "Change fruit to strawberry")
}
Text(text = "Value of MainViewModel.fruit: ${vm.fruit}")
}
}
/**
* When the done button of the keyboard is pressed, the focus is cleared from the field, then the onFocusChanged()
* modifier is called, and then onDone() is called.
*
* @param value the value shown in the field.
* @param onValueChange the lambda called when the field value is changed.
* @param onDone the lambda called when the done button of the keyboard is pressed or the focus goes away.
*/
@Composable
fun Field(
value: String,
onValueChange: (String) -> Unit,
onDone: () -> Unit
) {
val focusManager = LocalFocusManager.current
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.onFocusChanged {
if (!it.isFocused) onDone()
},
keyboardActions = KeyboardActions(
onDone = { focusManager.clearFocus() }
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
)
}
The value of fruit
can be changed typing in the textfield, pressing the buttons or with other methods in the Viewmodel. I can't figure out how to make fieldValue
(the value of the textField) equal to fruit
variable after calling the changeFruit()
function.
Thanks in advance to whoever helps me.
fruit
is already a state that is saved in the viewModel
, the problem here is you're initializing another state with remember
. Remember saves the value through the composition lifecycle, so your view is updated when fieldValue
is set in the onValueChange = { fieldValue = it }
lambda because setting a state triggers recomposition, but it's not updating when you set it programmatically because there is nothing to trigger the recomposition. There are 2 solutions to this:
1.You can use directly the state in the viewModel
and don't create another redundant state with remember.
@Composable
fun App() {
val vm: MainViewModel = viewModel()
Column {
Field(
value = vm.fruit,
onValueChange = { vm.changeFruit(it) }
)
}
}
2.Passing a key
to remember
forces it to recalculate the value that it stores and this will trigger recomposition too
@Composable
fun App() {
val vm: MainViewModel = viewModel()
var fieldValue by remember(key1 = vm.fruit) { mutableStateOf(vm.fruit) }
Column {
Field(
value = fieldValue,
onValueChange = { vm.changeFruit(it) }
)
}
}
An additional information for performance, passing the value
directly to your Composable will cause recomposition in the entire parent Composable, App()
in your case, because recomposition will start when there is a state read in its scope and this means redundant recompositions for your other Composables if they're not skippable
.
@Composable
fun Field(
provideValue: () -> String,
onValueChange: (String) -> Unit
) {
TextField(
value = provideValue(),
onValueChange = onValueChange
)
}
@Composable
fun App() {
Field(
provideValue = { vm.fruit },
onValueChange = { vm.changeFruit(it) }
)
}
This way state will be read in the scope of the provideValue()
lambda and not in the parent Composable, so recomposition will only happen for the Field()
Composable. This's called the lambda approach and it's recommended to use it to pass frequently changing states.