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

本页面提供了如何将基于值的 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 在检测到用户输入后立即调用。
    • 通过 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 以设置输入的最大长度。
  • 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 通过单个不可变类来定义屏幕或组件的 UI 状态,该类包含所有信息。

在这类应用中,通常按如下方式设计带有文本输入的登录屏幕等表单:

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 UI 状态不直接兼容。将 username: Stringpassword: String 替换为 username: TextFieldStatepassword: TextFieldState 可能会看起来很奇怪,因为 TextFieldState 本质上是可变数据结构。

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

MutableStateTextFieldState 这样的类是简单的状态持有者,由 Compose 的快照状态系统支持。它们与 StateFlowRxJava 等依赖项没有区别。因此,我们鼓励您重新评估如何在代码中应用“ViewModel 中没有 UI 依赖项”的原则。在 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 值。
  • LoginForm 可组合项中将这些 TextFieldState 对象传递给 TextFields

符合性方法

这些类型的架构更改并非总是容易。您可能没有自由进行这些更改,或者时间投入可能超过使用新 TextFields 的好处。在这种情况下,您仍然可以通过一些调整使用基于状态的文本字段。

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 仍将拥有 UI 的最新值,但其 uiState: StateFlow<UiState> 不会驱动 TextFields。
  • 您在 ViewModel 中实现的任何其他持久性逻辑都可以保持不变。