背景
我正在尝试弄清楚如何将默认值传递给 TextField 可组合项。我已经看到了一些解决方案,但我正在查看一些有关最广泛接受的解决方案的反馈。
示例
假设您有一个像这样的
ViewModel
:
class UserProfileViewModel : ViewModel() {
sealed class State {
data class UserDataFetched(
val firstName: String
// ...
) : State()
object ErrorFetchingData: State()
object ErrorUpdatingData: State()
object Loading: State()
// ...
}
val state = MutableLiveData<State>()
// ...
}
这个
ViewModel
用于一段 UI – 比方说,让您通过 TextField
更新用户名 – 看起来像这样:
val state by viewModel.state.observeAsState(initial = UserProfileViewModel.State.Loading)
MaterialTheme() {
UserProfileScreen(
state = state
)
}
@Composable
fun UserProfileScreen(
state: UserProfileViewModel.State,
) {
val userNameValue = remember { mutableStateOf(TextFieldValue()) }
Column {
TextField(
value = userNameValue.value,
onValueChange = {
userNameValue.value = it
}
)
//...
}
}
现在,当我第一次收到
State.UserDataFetched
事件提示给用户时,我想用我进入的 TextField
预先填充 firstName
。
我已经看到了一些解决方案,但我不确定哪一个最被广泛接受或为什么。
#1 使用标志变量
@Composable
fun UserProfileScreen(
state: UserProfileViewModel.State,
) {
val userHasModifiedText = remember { mutableStateOf(false) }
val userNameValue = remember { mutableStateOf(TextFieldValue()) }
if (state is UserProfileViewModel.State.UserDataFetched) {
if (!userHasModifiedText.value) {
userNameValue.value = TextFieldValue(state.firstName)
}
}
Column {
TextField(
value = userNameValue.value,
onValueChange = {
userNameValue.value = it
userHasModifiedText.value = true
}
)
//...
}
}
我们的想法是使用
userHasModifiedText
来跟踪用户是否在 TextField
中输入了任何内容 - 这样我们就可以避免在重组时更改值。
#2 使用派生状态
@Composable
fun UserProfileScreen(
state: UserProfileViewModel.State,
defaultFirstName: String? = null,
) {
val userNameString = remember { mutableStateOf<String?>(null) }
val userNameValue = remember {
derivedStateOf {
if (userNameString.value != null)
TextFieldValue(userNameString.value ?: "")
else
TextFieldValue(defaultFirstName ?: "")
}
}
Column {
TextField(
value = userNameValue,
onValueChange = {
userNameString.value = it
}
)
//...
}
}
摘自这个答案这里。
#3 使用 LaunchedEffect
@Composable
fun UserProfileScreen(
state: UserProfileViewModel.State
) {
val userNameValue = remember { mutableStateOf(TextFieldValue) }
LaunchedEffect(true) {
if (state is UserProfileViewModel.State.UserDataFetched) {
userNameValue.value = TextFieldValue(state.firstName)
}
}
Column {
TextField(
value = userNameValue.value,
onValueChange = {
userNameValue.value = it
}
)
//...
}
}
#4 有一个特定的 LiveData 属性
假设我们不使用该
sealed class
,而是通过 LiveData
属性来跟踪该字段的内容
class UserProfileViewModel : ViewModel() {
// ...
val firstName = MutableLiveData<String>()
// ...
}
然后我们会做类似的事情
val firstName by viewModel.firstName.observeAsState("")
MaterialTheme() {
UserProfileScreen(
firstName = firstName,
onFirstNameEditTextChanged = {
viewModel.firstName.value = it
}
)
}
@Composable
fun UserProfileScreen(
firstName: String,
onFirstNameEditTextChanged: ((String) -> Unit) = {}
) {
val userNameValue = remember { mutableStateOf(TextFieldValue) }
Column {
TextField(
value = userNameValue.value,
onValueChange = {
userNameValue.value = it
onFirstNameEditTextChanged(it.text)
}
)
//...
}
}
注释
LiveData
,因为这就是该项目现在正在使用的。切换到 State
或 Kotlin Flow
并不是我们路线图中的内容。编辑#1
由于这个问题已被标记为固执己见,让我完全清楚我希望从中得到什么。
我列出了我能找到的关于如何在文本字段上设置初始/默认值的所有解决方案。如果有更好的解决方案或我未在此处列出的解决方案可以更好地解决此问题,请随时分享并解释为什么它更好。
我认为“只需传递 ViewModel”之类的答案并不能完全解决问题。如果我在 Fragment 上使用此可组合项并且我的 ViewModel 范围仅限于整个 Activity,该怎么办?如果我关闭 Fragment,我不希望通过 ViewModel 记住 TextField 值。我也在寻找简单的东西,据我所知,如果可组合项是独立的,那就更好,所以我不确定将可组合项绑定到 ViewModel 是最好的解决方案。
我正在寻找一种在 Android 社区中被广泛接受的清晰、简洁的解决方案,以便我和我的团队可以采用它。
我的期望是,您打开屏幕,我们从某处获取默认名称的值,我们将其预先填充在文本字段上,仅此而已。如果用户删除整个文本,就这样。如果用户关闭屏幕并返回,那么我们会再次获取该值并再次预填充它。 如果这个问题的目标仍然不清楚,或者您认为可能需要讨论,请告诉我,我会尽力进一步澄清。
通过实现
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
,您可以使用Composable函数viewModel
来获取Composable中的viewModel。喜欢:
@Composable
fun MainScreen() {
// we only suggest getting the viewModel in `top-level` composable functions
// this can reduce coupling between composable functions
// and even improve the performance( ViewModel is not Stable for Compose Compiler)
val vm: UserProfileViewModel = viewModel()
// then you can use it
val state by vm.state.observeAsState(initial = UserProfileViewModel.State.Loading)
Detail(state)
}
在
Detail
中,您可以根据状态显示不同的UI
// Now we can use the state in the Detail composable function
// It dose not need to know where the state comes from, not associated with the ViewModel
// if you want to change the value of the ViewModel, you can pass parameters to the composable function
// like : updateNewName: (String) -> Unit
// and pass it like : updateNewName = { newName -> vm.updateNewName(newName) }
@Composable
fun Detail(
state: UserProfileViewModel.State,
) {
when (state) {
is UserProfileViewModel.State.UserDataFetched -> {
val userData = (state as UserProfileViewModel.State.UserDataFetched)
// UserDataFetchedUI(...)
}
UserProfileViewModel.State.ErrorFetchingData -> {
// ErrorFetchingDataUI(...)
}
UserProfileViewModel.State.ErrorUpdatingData -> {
// ErrorUpdatingDataUI(...)
}
UserProfileViewModel.State.Loading -> {
// LoadingUI(...)
}
}
}
如果您想在
real
项目中查看有关此主题的一些代码,您可以参考这里
以下是定义和更新
TextField
状态以防止输入问题的最佳实践。请检查此链接。处理默认值或实际值可以在 ViewModel
中实现,并通过状态将值发送回您的 UI/Jetpack Compose。
这是一个简单的例子:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val vm: SignUpViewModel by viewModels()
TestingAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
SignUpScreen(userName = vm.username,
vm::updateUsername)
}
}
}
}
}
class SignUpViewModel(private val userRepository: UserRepository) : ViewModel() {
var username by mutableStateOf("")
private set
fun updateUsername(input: String) {
username = input
}
/**
* the rest of code
* ...
*/
}
@Composable
fun SignUpScreen(userName: String, updateUserName: (name: String) -> Unit = {}) {
/*
* Here the rest of your composable code
* ...
* */
OutlinedTextField(
value = userName,
onValueChange = { username ->
updateUserName.invoke(username) }
/*...*/
)
}
如果您不打算在项目中从
LiveData
迁移到 State
,请将状态替换为 LiveData
。希望这个解决方案有帮助。