迁移到基于状态的文本字段

本页面提供了如何将基于值的 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, onValueChangeremember { 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()
  • 使用 InputTransformationonValueChange 中重新实现过滤逻辑。
  • 使用 InputTransformation 的接收器范围内的 TextFieldBuffer 更新 state
    • 系统会在检测到用户输入后立即调用 InputTransformation
    • 通过 TextFieldBuffer提出的更改会立即应用,从而避免软件键盘与TextField之间出现同步问题。InputTransformation

信用卡格式化程序 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,以设置输入内容的最大长度。
  • 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 方法。

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: Stringpassword: String 替换为 username: TextFieldStatepassword: TextFieldState 看起来可能很奇怪,因为 TextFieldState 本质上是可变的数据 结构。

一个常见的建议是避免将界面依赖项放入 ViewModel 中。 虽然这通常是一种不错的做法,但有时可能会被误解。 对于纯粹是数据结构且不携带任何界面元素(如 TextFieldState)的 Compose 依赖项,这一点尤其如此。

MutableStateTextFieldState 等类是简单的状态容器,由 Compose 的快照状态系统提供支持。它们与 StateFlowRxJava 等依赖项没有什么不同。因此,我们建议您重新评估如何在代码中应用“ViewModel 中没有界面依赖项”原则。在 ViewModel 中保留对 TextFieldState 的引用并非本质上是一种不好的做法。

我们建议您从 UiState 中提取 usernamepassword 等值,并在 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)
    }
}

  • 保持 ViewModelUiState 类不变。
  • 不要直接将状态提升到 ViewModel 并使其成为 TextFields 的真实来源,而是将 ViewModel 转换为简单的数据容器。
    • 为此,请通过在 LaunchedEffect 中收集 snapshotFlow 来观察对每个 TextFieldState.text 的更改。
  • 您的 ViewModel 仍将具有界面中的最新值,但其 uiState: StateFlow<UiState> 不会驱动 TextField
  • ViewModel 中实现的任何其他持久性逻辑都可以保持不变。