本页提供了有关如何将基于值的 TextField
迁移到基于状态的 TextField
的示例。如需了解基于值和基于状态的 TextField
之间的区别,请参阅配置文本字段页面。
基本用法
基于价值
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
基于状态
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- 将
value, onValueChange
和remember { mutableStateOf("")
} 替换为rememberTextFieldState()
。 - 将
singleLine = true
替换为lineLimits = TextFieldLineLimits.SingleLine
。
过滤 onValueChange
基于价值
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
基于状态
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- 将值回调循环替换为
rememberTextFieldState()
。 - 使用
InputTransformation
重新实现onValueChange
中的过滤逻辑。 - 使用
InputTransformation
的接收器范围内的TextFieldBuffer
来更新state
。- 系统会在检测到用户输入后立即调用
InputTransformation
。 - 由
InputTransformation
通过TextFieldBuffer
提出的更改会立即应用,从而避免软件键盘与TextField
之间的同步问题。
- 系统会在检测到用户输入后立即调用
信用卡格式设置工具 TextField
基于价值
@Composable fun OldTextFieldCreditCardFormatter() { var state by remember { mutableStateOf("") } TextField( value = state, onValueChange = { if (it.length <= 16) state = it }, visualTransformation = VisualTransformation { text -> // Making XXXX-XXXX-XXXX-XXXX string. var out = "" for (i in text.indices) { out += text[i] if (i % 4 == 3 && i != 15) out += "-" } TransformedText( text = AnnotatedString(out), offsetMapping = object : OffsetMapping { override fun originalToTransformed(offset: Int): Int { if (offset <= 3) return offset if (offset <= 7) return offset + 1 if (offset <= 11) return offset + 2 if (offset <= 16) return offset + 3 return 19 } override fun transformedToOriginal(offset: Int): Int { if (offset <= 4) return offset if (offset <= 9) return offset - 1 if (offset <= 14) return offset - 2 if (offset <= 19) return offset - 3 return 16 } } ) } ) }
基于状态
@Composable fun NewTextFieldCreditCardFormatter() { val state = rememberTextFieldState() TextField( state = state, inputTransformation = InputTransformation.maxLength(16), outputTransformation = OutputTransformation { if (length > 4) insert(4, "-") if (length > 9) insert(9, "-") if (length > 14) insert(14, "-") }, ) }
- 将
onValueChange
中的过滤替换为InputTransformation
,以设置输入的最大长度。- 请参阅通过
onValueChange
进行过滤部分。
- 请参阅通过
- 将
VisualTransformation
替换为OutputTransformation
,以添加短划线。- 使用
VisualTransformation
时,您需要负责创建带短划线的新文本,并计算视觉文本与后备状态之间的索引映射关系。 OutputTransformation
会自动处理偏移量映射。您只需使用OutputTransformation.transformOutput
接收器范围内的TextFieldBuffer
在正确的位置添加短划线即可。
- 使用
更新状态(简单)
基于价值
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
基于状态
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- 将值回调循环替换为
rememberTextFieldState()
。 - 使用
TextFieldState.setTextAndPlaceCursorAtEnd
更改值分配。
更新状态(复杂)
基于价值
@Composable fun OldTextFieldAddMarkdownEmphasis() { var markdownState by remember { mutableStateOf(TextFieldValue()) } Button(onClick = { // add ** decorations around the current selection, also preserve the selection markdownState = with(markdownState) { copy( text = buildString { append(text.take(selection.min)) append("**") append(text.substring(selection)) append("**") append(text.drop(selection.max)) }, selection = TextRange(selection.min + 2, selection.max + 2) ) } }) { Text("Bold") } TextField( value = markdownState, onValueChange = { markdownState = it }, maxLines = 10 ) }
基于状态
@Composable fun NewTextFieldAddMarkdownEmphasis() { val markdownState = rememberTextFieldState() LaunchedEffect(Unit) { // add ** decorations around the current selection markdownState.edit { insert(originalSelection.max, "**") insert(originalSelection.min, "**") selection = TextRange(originalSelection.min + 2, originalSelection.max + 2) } } TextField( state = markdownState, lineLimits = TextFieldLineLimits.MultiLine(1, 10) ) }
在此使用情形中,按钮会添加 Markdown 装饰,使光标周围或当前所选内容中的文字变为粗体。它还会在更改后保持选择位置。
- 将值回调循环替换为
rememberTextFieldState()
。 - 将
maxLines = 10
替换为lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
。 - 通过
TextFieldState.edit
调用更改了计算新TextFieldValue
的逻辑。- 通过根据当前选择拼接现有文本并在中间插入 Markdown 装饰来生成新的
TextFieldValue
。 - 此外,系统还会根据文本的新索引调整所选内容。
TextFieldState.edit
通过使用TextFieldBuffer
,可以更自然地编辑当前状态。- 选择明确定义了装饰的插入位置。
- 然后,调整选择,与
onValueChange
方法类似。
- 通过根据当前选择拼接现有文本并在中间插入 Markdown 装饰来生成新的
ViewModel StateFlow
架构
许多应用都遵循现代应用开发指南,该指南提倡使用 StateFlow
通过携带所有信息的单个不可变类来定义屏幕或组件的界面状态。
在这些类型的应用中,通常会按如下方式设计包含文本输入的登录屏幕等表单:
class LoginViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> get() = _uiState.asStateFlow() fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } } data class UiState( val username: String = "", val password: String = "" ) @Composable fun LoginForm( loginViewModel: LoginViewModel, modifier: Modifier = Modifier ) { val uiState by loginViewModel.uiState.collectAsStateWithLifecycle() Column(modifier) { TextField( value = uiState.username, onValueChange = { loginViewModel.updateUsername(it) } ) TextField( value = uiState.password, onValueChange = { loginViewModel.updatePassword(it) }, visualTransformation = PasswordVisualTransformation() ) } }
此设计非常适合使用 value,
onValueChange
状态提升范例的 TextFields
。不过,在文本输入方面,这种方法存在一些不可预测的缺点。有关此方法的深度同步问题的详细说明,请参阅在 Compose 中有效管理 TextField 的状态这篇博文。
问题在于,新的 TextFieldState
设计与 StateFlow
支持的 ViewModel 界面状态并不直接兼容。将 username: String
和 password: String
替换为 username: TextFieldState
和 password: TextFieldState
看起来可能很奇怪,因为 TextFieldState
本身就是可变的数据结构。
一个常见的建议是避免将界面依赖项放入 ViewModel
中。虽然这通常是一种良好的做法,但有时可能会被误解。对于纯粹是数据结构且不包含任何界面元素的 Compose 依赖项(例如 TextFieldState
),这一点尤其重要。
MutableState
或 TextFieldState
等类是简单的状态容器,由 Compose 的 Snapshot 状态系统提供支持。它们与 StateFlow
或 RxJava
等依赖项没有区别。因此,我们建议您重新评估如何在代码中应用“ViewModel 中无界面依赖项”原则。在 ViewModel
中保留对 TextFieldState
的引用本身并不是一种不好的做法。
推荐的简单方法
我们建议您从 UiState
中提取 username
或 password
等值,并在 ViewModel
中单独引用这些值。
class LoginViewModel : ViewModel() { val usernameState = TextFieldState() val passwordState = TextFieldState() } @Composable fun LoginForm( loginViewModel: LoginViewModel, modifier: Modifier = Modifier ) { Column(modifier) { TextField(state = loginViewModel.usernameState,) SecureTextField(state = loginViewModel.passwordState) } }
- 将
MutableStateFlow<UiState>
替换为几个TextFieldState
值。 - 将这些
TextFieldState
对象传递给LoginForm
可组合函数中的TextFields
。
一致性方法
这类架构变更并不总是那么容易。您可能无法自由进行这些更改,或者时间投入可能会超过使用新 TextField
的好处。在这种情况下,您只需稍作调整,即可继续使用基于状态的文本字段。
class LoginViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> get() = _uiState.asStateFlow() fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } } data class UiState( val username: String = "", val password: String = "" ) @Composable fun LoginForm( loginViewModel: LoginViewModel, modifier: Modifier = Modifier ) { val initialUiState = remember(loginViewModel) { loginViewModel.uiState.value } Column(modifier) { val usernameState = rememberTextFieldState(initialUiState.username) LaunchedEffect(usernameState) { snapshotFlow { usernameState.text.toString() }.collectLatest { loginViewModel.updateUsername(it) } } TextField(usernameState) val passwordState = rememberTextFieldState(initialUiState.password) LaunchedEffect(usernameState) { snapshotFlow { usernameState.text.toString() }.collectLatest { loginViewModel.updatePassword(it) } } SecureTextField(passwordState) } }
- 保持
ViewModel
和UiState
类相同。 - 不要直接将状态提升到
ViewModel
并使其成为TextFields
的可信来源,而是将ViewModel
变成一个简单的数据持有者。- 为此,请通过在
LaunchedEffect
中收集snapshotFlow
来观察每个TextFieldState.text
的变化。
- 为此,请通过在
- 您的
ViewModel
仍会包含界面中的最新值,但其uiState: StateFlow<UiState>
不会影响TextField
。 - 在
ViewModel
中实现的所有其他持久性逻辑可以保持不变。