如何通过viewModel和其他可组合项更改TextField的值?

huangapple go评论62阅读模式
英文:

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

问题

这是 MainViewModel.kt 部分:

class MainViewModel : ViewModel() {

    var fruit by mutableStateOf("") // 仅由 changeFruit() 方法更改
        private set

    /** 验证并更改 [fruit] 并返回结果。 */
    fun changeFruit(value: String): String {
        fruit = if (value == "apple") "banana" else value
        return fruit
    }

    // ...其他可以使用 changeFruit() 更改 fruit 变量的方法...
}

这是 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) }
        )

        // 我知道 onClick 可以是 { fieldValue = vm.changeFruit("apple") }
        // 和 { fieldValue = vm.changeFruit("strawberry") }
        // 但如果这些按钮放在其他可组合部分中怎么办?

        Button(onClick = { vm.changeFruit("apple") }) {
            Text(text = "将水果更改为苹果")
        }
        Button(onClick = { vm.changeFruit("strawberry") }) {
            Text(text = "将水果更改为草莓")

        }
        Text(text = "MainViewModel.fruit 的值: ${vm.fruit}")
    }
}

/**
 * 当键盘的完成按钮被按下时,焦点从字段中清除,然后调用 onFocusChanged() 修饰符,然后调用 onDone()。
 *
 * @param value 显示在字段中的值。
 * @param onValueChange 在更改字段值时调用的 lambda。
 * @param onDone 在键盘的完成按钮被按下或焦点消失时调用的 lambda。
 */
@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),
    )
}
英文:

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.

答案1

得分: 3

"fruit" 已经是保存在 viewModel 中的一个状态,问题在于你使用 remember 初始化了另一个状态。remember 会在组合生命周期中保存值,所以当你在 onValueChange = { fieldValue = it } lambda 中设置 fieldValue 时,视图会更新,因为设置状态会触发重新组合,但在程序中设置它时不会更新,因为没有东西触发重新组合。有两种解决方法:

  1. 你可以直接使用 viewModel 中的状态,不要创建多余的状态。
@Composable
fun App() {
    val vm: MainViewModel = viewModel()

    Column {
        Field(
            value = vm.fruit,
            onValueChange = { vm.changeFruit(it) }
        )
    }
}
  1. remember 传递一个 key 强制它重新计算存储的值,这也会触发重新组合。
@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) }
        )
    }
}

关于性能的额外信息,直接将 value 传递给你的 Composable 会导致整个父 Composable 重新组合,对于你的其他 Composable 如果它们不是 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) }
    )
}

这种方式会在 provideValue() lambda 的范围内读取状态,而不是在父 Composable 中读取,因此重新组合只会发生在 Field() Composable 中。这被称为 lambda 方法,建议使用它来传递频繁更改的状态。

英文:

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.

huangapple
  • 本文由 发表于 2023年5月14日 06:57:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/76245187.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定