Search code examples
androidkotlinandroid-jetpack-composetextfield

How to change TextField value also from viewModel and other composables?


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.


Solution

  • 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.