Compose OTP输入框快速输入时内容丢失问题排查
OTP输入框快速输入内容丢失问题的排查与修复
原代码存在的核心问题
- 硬编码状态列表长度:
remember中固定创建6个mutableStateOf(TextFieldValue),但组件参数支持自定义otpLength,当otpLength不等于6时会出现状态不匹配,同时快速输入时易引发状态索引混乱。 - 焦点移动的异步竞态:直接在
onValueChange中调用focusManager.moveFocus,Compose的状态更新是异步的,快速输入时焦点切换可能未完成就收到下一个输入事件,导致输入内容错位或丢失。 - 多字符输入处理逻辑不严谨:当用户快速输入多个字符时,仅取最后一个字符设置到下一个输入框,未考虑连续输入的状态堆叠,容易跳过部分输入内容。
- 按键事件拦截过度:
onKeyEvent始终返回true,阻止了系统默认的按键处理逻辑,可能干扰Backspace等按键的正常行为。
修复后的代码
@Composable @Preview fun OtpLayout(modifier: Modifier = Modifier, otpLength: Int = 6) { Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier.fillMaxWidth()) { val focusManager = LocalFocusManager.current // 根据otpLength动态创建状态列表,避免硬编码 val otpStates = remember(otpLength) { List(otpLength) { mutableStateOf(TextFieldValue(text = "", selection = TextRange(0))) } } repeat(otpLength) { index -> key(index) { BasicTextField( modifier = Modifier.onKeyEvent { event -> if (event.key == Key.Backspace && event.type == KeyEventType.KeyUp) { val currentValue = otpStates[index].value if (currentValue.text.isEmpty() && index > 0) { focusManager.moveFocus(FocusDirection.Left) // 仅处理该事件时返回true,其余交给系统处理 return@onKeyEvent true } } false }, maxLines = 1, value = otpStates[index].value, textStyle = TextStyle(color = Color.White, fontSize = 16.sp), keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), onValueChange = { newValue -> // 仅允许数字输入 if (!newValue.text.isDigitsOnly()) return@onValueChange val currentText = newValue.text // 处理单个字符输入场景 if (currentText.length <= 1) { otpStates[index].value = newValue // 输入非空且不是最后一个框,异步移动焦点 if (currentText.isNotEmpty() && index < otpLength - 1) { LaunchedEffect(Unit) { focusManager.moveFocus(FocusDirection.Right) } } // 输入清空且不是第一个框,异步移动焦点 else if (currentText.isEmpty() && index > 0) { LaunchedEffect(Unit) { focusManager.moveFocus(FocusDirection.Left) } } } else { // 处理多字符快速输入,拆分字符填充到后续框 val remainingChars = currentText.toList() otpStates[index].value = newValue.copy(text = remainingChars.first().toString()) // 依次填充后续输入框 for (i in index + 1 until otpLength) { if (i - index > remainingChars.size - 1) break otpStates[i].value = TextFieldValue( text = remainingChars[i - index].toString(), selection = TextRange(1) ) } // 移动焦点到最后一个被填充的框 val targetIndex = min(index + remainingChars.size, otpLength - 1) LaunchedEffect(Unit) { repeat(targetIndex - index) { focusManager.moveFocus(FocusDirection.Right) } } } }, decorationBox = { innerTextField -> Box( contentAlignment = Alignment.Center, modifier = Modifier .size(width = 42.dp, height = 52.dp) .border( width = 1.dp, color = PrimaryColor, shape = RoundedCornerShape(6.dp) ) .background(color = Gray, shape = RoundedCornerShape(6.dp)) ) { innerTextField() } } ) } } } }
关键改动说明
- 动态状态列表:根据
otpLength参数创建对应长度的状态列表,确保状态与UI输入框数量完全匹配。 - 异步焦点处理:使用
LaunchedEffect包裹焦点移动逻辑,确保状态更新完成后再执行焦点切换,解决快速输入时的异步竞态问题。 - 完善多字符输入处理:当用户快速输入多个字符时,自动拆分字符并填充到后续OTP输入框,同时将焦点定位到最后一个被填充的框,避免内容丢失。
- 合理拦截按键事件:仅在处理Backspace清空当前框并需要左移焦点时返回
true,其余情况交给系统处理默认逻辑,避免干扰正常输入行为。
内容的提问来源于stack exchange,提问作者sarthak gupta
相关产品推荐
相关产品推荐

