22、表单与输入:OutlinedTextField、键盘选项、输入验证

表单开发,说白了就是跟用户「要数据」。我在项目里见过太多因为输入框没处理好,导致用户填到一半崩溃退出的情况。嗯,今天我们就来聊聊 Compose 里怎么把表单做得既好用又靠谱。

22.1 OutlinedTextField 的基本用法

Compose 提供了两种文本输入框:TextFieldOutlinedTextField。我个人习惯用 OutlinedTextField,因为它有边框,视觉上更清晰,用户一眼就知道哪里可以输入。

@Composable
fun BasicOutlinedTextField() {
    var text by remember { mutableStateOf("") }

    OutlinedTextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("用户名") },
        placeholder = { Text("请输入用户名") },
        singleLine = true
    )
}

这里有个细节:valueonValueChange 是必须的。为什么?因为 Compose 是声明式 UI,状态由你管理,框架只负责渲染。你想想看,如果 value 不更新,用户敲键盘屏幕不会有任何变化。

💡 我的习惯: 所有输入框都用 remember 包裹状态,避免重组时丢失数据。如果是列表中的输入框,记得用 rememberSaveable 来应对配置变更。

22.2 键盘选项:让输入更智能

键盘类型选对了,用户体验能提升一大截。我曾经在一个金融 App 里看到数字输入框弹出全键盘,用户气得直接卸载了。

Compose 通过 keyboardOptions 参数控制键盘行为:

OutlinedTextField(
    value = amount,
    onValueChange = { amount = it },
    label = { Text("金额") },
    keyboardOptions = KeyboardOptions(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Done
    ),
    keyboardActions = KeyboardActions(
        onDone = { /* 处理完成事件 */ }
    )
)
KeyboardType 适用场景
Text 普通文本
Number 纯数字(如金额、年龄)
Email 邮箱地址(带 @ 和 . 快捷键)
Password 密码输入(自动隐藏字符)
Phone 电话号码(数字键盘 + 拨号键)

还有 imeAction,它控制键盘右下角的按钮文字。比如搜索框用 ImeAction.Search,登录表单用 ImeAction.Next 跳到下一个输入框。嗯,这里要注意:imeAction 只是改变按钮外观,实际逻辑要写在 keyboardActions 里。

⚠️ 避坑指南: 我曾经遇到一个 Bug——设置了 KeyboardType.Number 但用户还能输入小数点。后来发现 Number 类型默认允许小数。如果只要整数,用 NumberPassword 或者自己过滤 onValueChange

22.3 输入验证:别让用户乱填

验证分两种:实时验证和提交时验证。我个人倾向实时验证,用户边输边提示,体验更好。

22.3.1 实时验证示例

@Composable
fun EmailInput() {
    var email by remember { mutableStateOf("") }
    var isError by remember { mutableStateOf(false) }
    val errorMessage = "邮箱格式不正确"

    OutlinedTextField(
        value = email,
        onValueChange = {
            email = it
            isError = !isValidEmail(it)
        },
        label = { Text("邮箱") },
        isError = isError,
        supportingText = {
            if (isError) {
                Text(text = errorMessage, color = MaterialTheme.colorScheme.error)
            }
        }
    )
}

fun isValidEmail(email: String): Boolean {
    return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()
}

看到 supportingText 了吗?这是 Compose 1.3 之后推荐的做法,替代了旧的 error 参数。它既能显示错误提示,也能显示普通帮助文字,灵活很多。

22.3.2 表单整体验证

多个输入框时,我习惯用一个状态类统一管理:

data class FormState(
    val username: String = "",
    val password: String = "",
    val usernameError: String? = null,
    val passwordError: String? = null,
    val isSubmitting: Boolean = false
)

@Composable
fun LoginForm() {
    var formState by remember { mutableStateOf(FormState()) }

    fun validate(): Boolean {
        var valid = true
        formState = formState.copy(
            usernameError = if (formState.username.length < 3) "用户名至少3个字符" else null,
            passwordError = if (formState.password.length < 6) "密码至少6个字符" else null
        )
        valid = formState.usernameError == null && formState.passwordError == null
        return valid
    }

    Column(modifier = Modifier.padding(16.dp)) {
        OutlinedTextField(
            value = formState.username,
            onValueChange = { formState = formState.copy(username = it) },
            label = { Text("用户名") },
            isError = formState.usernameError != null,
            supportingText = { formState.usernameError?.let { Text(it) } }
        )
        Spacer(modifier = Modifier.height(8.dp))
        OutlinedTextField(
            value = formState.password,
            onValueChange = { formState = formState.copy(password = it) },
            label = { Text("密码") },
            visualTransformation = PasswordVisualTransformation(),
            isError = formState.passwordError != null,
            supportingText = { formState.passwordError?.let { Text(it) } }
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(
            onClick = {
                if (validate()) {
                    // 执行登录
                }
            },
            enabled = !formState.isSubmitting
        ) {
            Text("登录")
        }
    }
}
🔑 核心思路:data class 管理表单状态,每个字段的错误信息独立存储。这样验证逻辑清晰,UI 更新也精准——哪个字段错了就只显示哪个的错误。

22.4 视觉反馈与焦点管理

用户输入时,焦点切换和视觉反馈很重要。我记得有个项目,用户填完密码按回车,键盘没消失,也没跳到登录按钮,用户以为卡死了。

val focusManager = LocalFocusManager.current

OutlinedTextField(
    // ...
    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
    keyboardActions = KeyboardActions(
        onDone = { focusManager.clearFocus() }
    )
)

LocalFocusManager 可以手动控制焦点。比如登录成功后自动收起键盘,或者按「下一项」时跳到下一个输入框。

颜色方面,OutlinedTextField 默认使用主题色。你可以通过 colors 参数自定义:

OutlinedTextField(
    // ...
    colors = OutlinedTextFieldDefaults.colors(
        focusedBorderColor = Color.Blue,
        unfocusedBorderColor = Color.Gray,
        errorBorderColor = Color.Red,
        cursorColor = Color.Blue
    )
)

22.5 知识体系总览

下面这张图总结了本章的核心内容,你可以对照着回顾:

表单与输入知识体系 OutlinedTextField 基础输入框组件 键盘选项 KeyboardOptions / Actions 输入验证 实时 + 提交验证 核心属性 • value / onValueChange • label / placeholder • singleLine / maxLines • visualTransformation 键盘类型 • Text / Number / Email • Password / Phone • ImeAction 控制按钮 • focusManager 管理焦点 验证策略 • isError + supportingText • data class 统一管理 • 实时验证 vs 提交验证 • 正则 / Patterns 工具类 ⚠️ 常见陷阱 1. 忘记用 remember 管理状态 → 输入框不响应 2. 键盘类型选错 → 用户输入体验差 3. 验证逻辑写在 Composable 里 → 重组频繁,性能下降

22.6 实战建议

最后分享几个我在项目中沉淀下来的经验:

  • 状态提升: 把表单状态提到 ViewModel 里,用 StateFlow 管理。这样屏幕旋转不会丢数据,测试也方便。
  • 防抖处理: 实时验证时,比如检查用户名是否被注册,加个 300ms 的防抖。不然用户每敲一个字母就发一次请求,服务器会报警的。
  • 无障碍支持: 给输入框加 contentDescription,视障用户靠读屏软件也能操作。
  • 错误提示要友好: 别说「格式错误」,要说「邮箱地址需要包含 @ 符号」。用户不是程序员,看不懂正则表达式。

嗯,表单开发看似简单,但细节决定成败。把上面这些点都照顾到了,你的 App 输入体验绝对能甩开大多数竞品。