You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Compose:focusRequester.requestFocus()触发父布局未放置异常

Compose中FocusRequester引发BringIntoViewRequester异常问题解析

问题场景

我是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让它进入视野。

你的代码里有两个关键诱因:

  1. 用于接收焦点的TextField被设置为.size(0.dp),属于不可见状态,系统尝试滚动让它进入视野时,父组件(BottomSheet)可能还没完成布局,导致位置计算失败。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 11:17:00