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

Jetpack Compose:AlertDialog中TextField输入时失焦问题求助

问题描述

我有一个动态高度的AlertDialog,里面包含TextField组件,会执行非空校验等基础验证逻辑。校验失败时,TextField下方会显示警告信息;用户在TextField中输入文本后,警告会自动消失。但现在有个问题:当警告信息已经显示时,用户开始输入内容,TextField会莫名失去焦点。当前使用的Compose版本是compose:1.2.0-alpha04,求告知问题原因及解决办法,相关代码如下:

AlertDialog 代码

AlertDialog(
    properties = DialogProperties(usePlatformDefaultWidth = false),
    modifier = Modifier.width(250.dp),
    onDismissRequest = { showAddMatchDialog = false },
    buttons = {
        var player1Name by rememberSaveable { mutableStateOf("") }
        var player1NameError by rememberSaveable { mutableStateOf(false) }
        var player1Score by rememberSaveable { mutableStateOf("") }
        var player1ScoreError by rememberSaveable { mutableStateOf(false) }

        Column(
            modifier = Modifier.padding(top = 8.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = stringResource(R.string.add_match_dialog_title),
                fontSize = 20.sp,
                fontWeight = FontWeight.SemiBold
            )

            Spacer(Modifier.height(4.dp))

            PlayerRow(
                nameLabel = stringResource(R.string.player1_name),
                name = player1Name,
                isNameError = player1NameError,
                onNameClear = { player1Name = "" },
                onNameChange = {
                    player1Name = it
                    player1NameError = false
                },
                score = player1Score,
                isScoreError = player1ScoreError,
                onScoreChange = {
                    player1Score = it
                    player1ScoreError = false
                }
            )

            Spacer(Modifier.height(8.dp))

            // Same thing for player 2
        }
    }
)

PlayerRow 代码

@Composable
fun PlayerRow(
    nameLabel: String,
    name: String,
    isNameError: Boolean,
    onNameClear: () -> Unit,
    onNameChange: (String) -> Unit,
    score: String,
    isScoreError: Boolean,
    onScoreChange: (String) -> Unit
) {
    Column {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            // Name TextField
            Column {
                Text(
                    text = nameLabel,
                    style = MaterialTheme.typography.subtitle2
                )

                Spacer(Modifier.height(4.dp))

                Box {
                    BasicTextField(
                        modifier = Modifier
                            .width(TEXT_FIELD_WIDTH.dp)
                            .height(TEXT_FIELD_HEIGHT.dp)
                            .background(
                                color = GrayLight,
                                shape = roundedCornerShape
                            )
                            .then(
                                if (isNameError) {
                                    Modifier.border(
                                        width = 1.dp,
                                        color = Warning,
                                        shape = roundedCornerShape
                                    )
                                } else {
                                    Modifier
                                }
                            )
                            .padding(start = 8.dp, end = 8.dp, top = 6.dp),
                        value = name,
                        onValueChange = onNameChange,

                        singleLine = true,
                    )

                    Icon(
                        modifier = Modifier
                            .padding(start = (TEXT_FIELD_WIDTH - 25).dp, top = 3.dp)
                            .ripplelessClickable { onNameClear() },
                        imageVector = Icons.Default.Clear,
                        contentDescription = "",
                        tint = Gray
                    )
                }
            }

            // Score TextField
            Column {
                Text(
                    text = stringResource(R.string.score),
                    style = MaterialTheme.typography.subtitle2
                )
                Spacer(Modifier.height(4.dp))
                BasicTextField(
                    modifier = Modifier
                        .size(TEXT_FIELD_HEIGHT.dp)
                        .background(
                            color = GrayLight,
                            shape = roundedCornerShape
                        )
                        .then(
                            if (isScoreError) {
                                Modifier.border(
                                    width = 1.dp,
                                    color = Warning,
                                    shape = roundedCornerShape
                                )
                            } else {
                                Modifier
                            }
                        )
                        .padding(start = 6.dp, end = 6.dp, top = 6.dp),
                    value = score,
                    onValueChange = onScoreChange,
                    keyboardOptions = KeyboardOptions(
                        keyboardType = KeyboardType.Number
                    ),
                    singleLine = true,
                )
            }
        }

        if (isNameError) {
            Text(
                modifier = Modifier.padding(start = 20.dp),
                text = stringResource(R.string.enter_player_name),
                color = Warning,
                fontSize = 14.sp
            )
        }

        if (isScoreError) {
            Text(
                modifier = Modifier.padding(start = 20.dp),
                text = stringResource(R.string.enter_player_score),
                color = Warning,
                fontSize = 14.sp
            )
        }
    }
}

问题原因
  1. 早期Compose版本bug:你使用的1.2.0-alpha04是未稳定的测试版本,Dialog组件在布局动态变化时存在焦点管理缺陷。当警告文本显示/隐藏导致Dialog高度变化时,重组过程中TextField的焦点会被意外清除。
  2. 布局重组触发焦点丢失:输入文本后isNameError/isScoreError变为false,警告文本消失,Dialog高度发生变化,触发整个Dialog区域的重组,TextField的焦点状态未被正确保留。
  3. 状态位置不合理:状态变量(如player1NameError)被放在AlertDialog的buttons参数内部,buttons属于Dialog的局部content区域,重组时更容易影响内部组件的状态和焦点。
解决办法

1. 升级Compose到稳定版本

这是最直接的解决方案,后续的Compose稳定版(如1.2.1及以上)已修复大量Dialog和焦点相关的bug。在build.gradle中同步升级依赖:

implementation "androidx.compose.ui:ui:1.2.1"
implementation "androidx.compose.material:material:1.2.1"
// 其他Compose相关依赖同步升级到对应版本

2. 固定Dialog最小高度

避免警告显示/隐藏导致Dialog高度剧烈变化,减少重组对焦点的影响。给AlertDialog的modifier添加heightIn(min = ...):

AlertDialog(
    properties = DialogProperties(usePlatformDefaultWidth = false),
    modifier = Modifier.width(250.dp).heightIn(min = 300.dp), // 根据实际布局设置合适的最小高度
    onDismissRequest = { showAddMatchDialog = false },
    // 其他代码不变
)

3. 手动管理TextField焦点

使用FocusRequester保存焦点状态,在重组后手动请求焦点。修改PlayerRow中的BasicTextField:

@Composable
fun PlayerRow(
    // 原有参数不变
) {
    val nameFocusRequester = remember { FocusRequester() }
    // 监听错误状态变化,当错误消失时重新请求焦点
    LaunchedEffect(isNameError) {
        if (!isNameError) {
            nameFocusRequester.requestFocus()
        }
    }

    Column {
        Row(/* 原有代码不变 */) {
            // Name TextField
            Column {
                // 原有代码不变
                Box {
                    BasicTextField(
                        modifier = Modifier
                            .focusRequester(nameFocusRequester)
                            .focusable()
                            // 原有modifier属性不变
                        ,
                        // 其他参数不变
                    )
                    // 清除按钮代码不变
                }
            }
            // 分数输入框可同理添加scoreFocusRequester处理
        }
        // 警告文本代码不变
    }
}

4. 调整状态变量位置

把状态变量从buttons内部移到AlertDialog外部,减少局部重组的影响:

// 在AlertDialog外部定义状态
var player1Name by rememberSaveable { mutableStateOf("") }
var player1NameError by rememberSaveable { mutableStateOf(false) }
var player1Score by rememberSaveable { mutableStateOf("") }
var player1ScoreError by rememberSaveable { mutableStateOf(false) }

AlertDialog(
    properties = DialogProperties(usePlatformDefaultWidth = false),
    modifier = Modifier.width(250.dp),
    onDismissRequest = { showAddMatchDialog = false },
    buttons = {
        Column(
            modifier = Modifier.padding(top = 8.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            // 原有代码不变,直接使用外部定义的状态变量
        }
    }
)

内容的提问来源于stack exchange,提问作者kc_dev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 03:33:28