Android Compose:focusRequester.requestFocus()触发父布局未放置异常
问题场景
我是Android Compose新手,维护项目时遇到运行时崩溃。代码中通过LaunchedEffect请求焦点的部分抛出异常,堆栈信息提示IllegalStateException: Expected BringIntoViewRequester to not be used before parents are placed.,但我并没有手动配置过BringIntoViewRequester。
错误代码片段
@ExperimentalMaterialNavigationApi @ExperimentalAnimationApi @ExperimentalMaterialApi @ExperimentalComposeUiApi @Composable fun BottomSheetContent() { BaseBottomSheetComposable(showBar = true) { Column { val otpCtaEnabled = remember { mViewModel.enabledOtpCtaState } Text( text = stringResource(id = R.string.text_login_otp_title) .plus(" ") .plus(mViewModel.getMobileNumber()), style = typography.body1.copy(fontSize = 14.sp, color = angrezi), modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.dp) ) val focusRequester = remember { FocusRequester() } val editValue = remember { mViewModel.loginOtp } val keyboard = LocalSoftwareKeyboardController.current TextField( value = editValue.value, onValueChange = { if (it.length <= OTP_LENGTH) { editValue.value = it mViewModel.onOtpTextChanged(it) } if (it.length == OTP_LENGTH) { keyboard?.hide() } }, modifier = Modifier .size(0.dp) .focusRequester(focusRequester), keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Number ) ) // 引发崩溃的代码 LaunchedEffect(true) { focusRequester.requestFocus() } Row( modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp), horizontalArrangement = Arrangement.Center ) { (0 until OTP_LENGTH).map { index -> OtpCell( modifier = Modifier .width(56.dp) .height(56.dp) .padding(horizontal = 4.dp) .clip(RoundedCornerShape(12.dp)) .background(MonetGray0) .focusRequester(focusRequester) .clickable { focusRequester.requestFocus() keyboard?.show() }, value = editValue.value.getOrNull(index)?.toString() ?: "", isCursorVisible = editValue.value.length == index ) } } ButtonPrimary(text = stringResource(id = R.string.text_otp_cta), state = otpCtaEnabled, onCtaClick = { mViewModel.onOtpCtaClicked() }) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { OtpRetryText() CircularProgressTimer(percentage = 100f, animDuration = COUNT_DOWN_TIME_IN_MILLIS) } } } }
崩溃堆栈信息
AndroidRuntime E FATAL EXCEPTION: main Process: in.myapp.crew, PID: 32709 java.lang.IllegalStateException: Expected BringIntoViewRequester to not be used before parents are placed. at androidx.compose.foundation.gestures.ContentInViewModifier.calculateRectForParent(ContentInViewModifier.kt:117) at androidx.compose.foundation.relocation.BringIntoViewResponderNode$bringChildIntoView$parentRect$1.invoke(BringIntoViewResponder.kt:160) at androidx.compose.foundation.relocation.BringIntoViewResponderNode$bringChildIntoView$parentRect$1.invoke(BringIntoViewResponder.kt:160) at androidx.compose.foundation.relocation.BringIntoViewResponder_androidKt$defaultBringIntoViewParent$1.bringChildIntoView(BringIntoViewResponder.android.kt:31) at androidx.compose.foundation.relocation.BringIntoViewResponderNode$bringChildIntoView$2$2.invokeSuspend(BringIntoViewResponder.kt:179) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81) at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41) at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run(AndroidUiDispatcher.android.kt:57) at android.os.Handler.handleCallback(Handler.java:942) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:201) at android.os.Looper.loop(Looper.java:288) at android.app.ActivityThread.main(ActivityThread.java:7872) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [androidx.compose.ui.platform.MotionDurationScaleImpl@86a65ab, androidx.compose.runtime.BroadcastFrameClock@d899408, StandaloneCoroutine{Cancelling}@4dea7a1, AndroidUiDispatcher@1669dc6]
项目依赖版本:
- Gradle:
gradle-7.5 - Kotlin版本:
1.8.21 - Android Gradle插件:
7.4.2 - Compose版本均为1.4.3(含foundation、material、ui等模块)
问题解析
错误含义
Expected BringIntoViewRequester to not be used before parents are placed 意思是BringIntoViewRequester不能在父组件完成布局(被放置到界面上)之前调用。
Compose的组件渲染分测量、布局、绘制三个阶段,只有当父组件完成布局阶段后,子组件的位置、尺寸等信息才会确定。BringIntoViewRequester的作用是让组件滚动到可视区域,它需要依赖父组件的布局信息才能计算滚动位置,如果父组件还没布局完成,就会抛出这个异常。
为什么FocusRequester会触发这个问题?
你虽然没手动使用BringIntoViewRequester,但FocusRequester.requestFocus()内部会自动触发自动滚动到可视区域的逻辑:当获取焦点的组件不在当前可视范围内时,Compose会自动调用BringIntoViewRequester让它进入视野。
你的代码里有两个关键诱因:
- 用于接收焦点的
TextField被设置为.size(0.dp),属于不可见状态,系统尝试滚动让它进入视野时,父组件(BottomSheet)可能还没完成布局,导致位置计算失败。 LaunchedEffect(true)会在Compose首次组合时立即执行,此时BottomSheet作为弹窗组件,布局完成时机比普通页面晚,父组件的布局信息尚未确定,调用requestFocus()触发的BringIntoView操作就会抛出异常。
解决方案
方案1:延迟焦点请求,等待布局完成
通过withFrameNanos等待一帧,确保父组件完成布局后再请求焦点:
LaunchedEffect(Unit) { // 等待一帧,让布局流程完成 withFrameNanos { } focusRequester.requestFocus() }
方案2:禁用自动滚动到可视区域的行为
给TextField添加focusProperties,手动禁用自动BringIntoView逻辑:
TextField( // ...其他参数 modifier = Modifier .size(0.dp) .focusRequester(focusRequester) .focusProperties { // 禁用自动滚动到视图内的行为 bringIntoViewRequester = null }, )
方案3:优化OTP输入实现
当前用隐藏TextField+自定义OtpCell的方式容易引发焦点问题,建议改用更规范的实现:
- 直接使用多个独立的TextField实现OTP输入
- 采用Compose官方推荐的OTP输入组件(或成熟的第三方库),避免隐藏组件带来的焦点、布局问题
补充说明
这个问题在Compose 1.x早期版本(比如1.4.x)中较为常见,尤其是在BottomSheet、Dialog这类弹窗组件中请求焦点时。升级到Compose 1.5及以上版本可能会修复底层的布局时序问题,如果项目允许升级,可以优先尝试。
内容的提问来源于stack exchange,提问作者Nitinraj Vallyedath

