Jetpack Compose表单验证:如何将焦点定位到首个错误字段?
实现Jetpack Compose表单首个错误字段自动聚焦
1. ViewModel层维护错误状态与首个错误标识
在ViewModel里,为每个表单字段单独维护错误信息,同时新增一个状态来记录视觉顺序上的首个错误字段标识(比如用字符串或枚举区分不同字段)。校验时严格按照表单显示的顺序判断,第一个触发错误的字段就写入标识。
示例代码:
class FormViewModel : ViewModel() { // 输入字段状态 val username = mutableStateOf("") val password = mutableStateOf("") val email = mutableStateOf("") // 字段错误信息 private val _usernameError = mutableStateOf<String?>(null) val usernameError: State<String?> = _usernameError private val _passwordError = mutableStateOf<String?>(null) val passwordError: State<String?> = _passwordError private val _emailError = mutableStateOf<String?>(null) val emailError: State<String?> = _emailError // 首个错误字段标识 private val _firstErrorField = mutableStateOf<String?>(null) val firstErrorField: State<String?> = _firstErrorField fun validateForm() { // 重置所有错误状态 _usernameError.value = null _passwordError.value = null _emailError.value = null _firstErrorField.value = null // 按表单显示顺序校验,第一个出错的字段记录标识 if (username.value.isBlank()) { _usernameError.value = "用户名不能为空" _firstErrorField.value = "username" } else if (password.value.length < 6) { _passwordError.value = "密码长度不能少于6位" _firstErrorField.value = "password" } else if (!email.value.contains("@")) { _emailError.value = "请输入合法邮箱" _firstErrorField.value = "email" } } }
2. UI层绑定焦点请求器并触发聚焦
为每个输入框创建FocusRequester,通过LaunchedEffect监听ViewModel的首个错误字段标识变化,一旦有值就自动请求对应输入框的焦点。
示例代码:
@Composable fun FormScreen(viewModel: FormViewModel = viewModel()) { val focusManager = LocalFocusManager.current // 为每个字段绑定焦点请求器 val usernameFocus = FocusRequester() val passwordFocus = FocusRequester() val emailFocus = FocusRequester() // 监听首个错误字段,触发聚焦 LaunchedEffect(viewModel.firstErrorField.value) { when (viewModel.firstErrorField.value) { "username" -> usernameFocus.requestFocus() "password" -> passwordFocus.requestFocus() "email" -> emailFocus.requestFocus() } } Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { // 用户名输入框 OutlinedTextField( value = viewModel.username.value, onValueChange = { viewModel.username.value = it }, label = { Text("用户名") }, modifier = Modifier .fillMaxWidth() .focusRequester(usernameFocus), isError = viewModel.usernameError.value != null, supportingText = { viewModel.usernameError.value?.let { Text(it) } } ) // 密码输入框 OutlinedTextField( value = viewModel.password.value, onValueChange = { viewModel.password.value = it }, label = { Text("密码") }, modifier = Modifier .fillMaxWidth() .focusRequester(passwordFocus), isError = viewModel.passwordError.value != null, supportingText = { viewModel.passwordError.value?.let { Text(it) } }, visualTransformation = PasswordVisualTransformation() ) // 邮箱输入框 OutlinedTextField( value = viewModel.email.value, onValueChange = { viewModel.email.value = it }, label = { Text("邮箱") }, modifier = Modifier .fillMaxWidth() .focusRequester(emailFocus), isError = viewModel.emailError.value != null, supportingText = { viewModel.emailError.value?.let { Text(it) } } ) Button( onClick = { viewModel.validateForm() }, modifier = Modifier.fillMaxWidth() ) { Text("提交") } } }
3. 优化小技巧
如果表单字段较多,可以用Map统一管理焦点请求器,减少重复代码:
val focusRequesters = mapOf( "username" to FocusRequester(), "password" to FocusRequester(), "email" to FocusRequester() ) // LaunchedEffect里简化为 viewModel.firstErrorField.value?.let { fieldKey -> focusRequesters[fieldKey]?.requestFocus() }
内容的提问来源于stack exchange,提问作者Lena
相关产品推荐
相关产品推荐

