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

Jetpack Compose:TextField装饰器内IconButton不重组问题

解决BasicSecureTextField装饰器内IconButton不随状态更新的问题

问题场景:
嵌套在BasicSecureTextField装饰器里的密码显示/隐藏图标按钮,在isPasswordVisible状态变化时不会切换图标。虽然密码的显示/隐藏功能正常,但状态变化时IconButton所在的代码块完全没执行。

原因分析:
BasicSecureTextField的decorator参数默认会被Compose当作稳定的lambda处理,它不会主动追踪lambda内部捕获的外部状态变化。所以当isPasswordVisible改变时,Compose不会触发decorator内代码的重组,导致图标无法更新。

解决方案:

方法一:用rememberUpdatedState包装状态

通过rememberUpdatedState把需要追踪的状态包装起来,确保decorator能获取到最新的状态值:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PasswordTextField(
    state: TextFieldState,
    isPasswordVisible: Boolean,
    onTogglePasswordVisibility: () -> Unit,
    hint: String,
    title: String?,
    modifier: Modifier = Modifier
) {
    var isFocused by remember { mutableStateOf(false) }
    // 包装状态,保证decorator能拿到最新值
    val updatedVisibleState = rememberUpdatedState(isPasswordVisible)
    val updatedToggleAction = rememberUpdatedState(onTogglePasswordVisibility)

    Column(modifier = modifier) {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            title?.let {
                Text(
                    text = it,
                    style = MaterialTheme.typography.titleSmall,
                )
            }
        }
        Spacer(modifier = Modifier.height(4.dp))
        BasicSecureTextField(
            state = state,
            textObfuscationMode = if (isPasswordVisible) TextObfuscationMode.Visible else TextObfuscationMode.Hidden,
            textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onBackground),
            cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
            modifier = Modifier
                .clip(RoundedCornerShape(16.dp))
                .background(MaterialTheme.colorScheme.surface)
                .border(1.dp, Color.Transparent, RoundedCornerShape(16.dp))
                .padding(12.dp)
                .onFocusChanged { isFocused = it.isFocused },
            decorator = { innerBox ->
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Icon(
                        imageVector = PasswordIcon,
                        contentDescription = null,
                        tint = MaterialTheme.colorScheme.onSurface
                    )

                    Spacer(modifier = Modifier.width(16.dp))

                    Box(modifier = Modifier.weight(1f)) {
                        if (state.text.isEmpty() && !isFocused) {
                            Text(
                                text = hint,
                                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f),
                                modifier = Modifier.fillMaxWidth()
                            )
                        }
                        innerBox()
                    }

                    IconButton(
                        onClick = { updatedToggleAction.value() },
                        modifier = Modifier.height(24.dp)
                    ) {
                        Icon(
                            imageVector = if (updatedVisibleState.value) EyeOpenIcon else EyeCloseIcon,
                            contentDescription = if (updatedVisibleState.value) {
                                stringResource(id = R.string.show_password)
                            } else {
                                stringResource(id = R.string.hide_password)
                            },
                            tint = MaterialTheme.colorScheme.onSurface
                        )
                    }
                }
            }
        )
    }
}

方法二:提取装饰器内容为独立Composable

把decorator里的布局逻辑抽成单独的@Composable函数,让Compose能正确追踪状态依赖:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PasswordTextField(
    state: TextFieldState,
    isPasswordVisible: Boolean,
    onTogglePasswordVisibility: () -> Unit,
    hint: String,
    title: String?,
    modifier: Modifier = Modifier
) {
    var isFocused by remember { mutableStateOf(false) }

    Column(modifier = modifier) {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            title?.let {
                Text(
                    text = it,
                    style = MaterialTheme.typography.titleSmall,
                )
            }
        }
        Spacer(modifier = Modifier.height(4.dp))
        BasicSecureTextField(
            state = state,
            textObfuscationMode = if (isPasswordVisible) TextObfuscationMode.Visible else TextObfuscationMode.Hidden,
            textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onBackground),
            cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
            modifier = Modifier
                .clip(RoundedCornerShape(16.dp))
                .background(MaterialTheme.colorScheme.surface)
                .border(1.dp, Color.Transparent, RoundedCornerShape(16.dp))
                .padding(12.dp)
                .onFocusChanged { isFocused = it.isFocused },
            decorator = { innerBox ->
                PasswordDecoratorContent(
                    innerBox = innerBox,
                    isPasswordVisible = isPasswordVisible,
                    onToggle = onTogglePasswordVisibility,
                    hint = hint,
                    isFocused = isFocused,
                    state = state
                )
            }
        )
    }
}

@Composable
private fun PasswordDecoratorContent(
    innerBox: @Composable () -> Unit,
    isPasswordVisible: Boolean,
    onToggle: () -> Unit,
    hint: String,
    isFocused: Boolean,
    state: TextFieldState
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            imageVector = PasswordIcon,
            contentDescription = null,
            tint = MaterialTheme.colorScheme.onSurface
        )

        Spacer(modifier = Modifier.width(16.dp))

        Box(modifier = Modifier.weight(1f)) {
            if (state.text.isEmpty() && !isFocused) {
                Text(
                    text = hint,
                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f),
                    modifier = Modifier.fillMaxWidth()
                )
            }
            innerBox()
        }

        IconButton(
            onClick = onToggle,
            modifier = Modifier.height(24.dp)
        ) {
            Icon(
                imageVector = if (isPasswordVisible) EyeOpenIcon else EyeCloseIcon,
                contentDescription = if (isPasswordVisible) {
                    stringResource(id = R.string.show_password)
                } else {
                    stringResource(id = R.string.hide_password)
                },
                tint = MaterialTheme.colorScheme.onSurface
            )
        }
    }
}

两种方法都能让Compose感知到isPasswordVisible的变化,触发IconButton的重组来切换图标。方法一适合快速修复,方法二更利于代码维护,尤其是装饰器逻辑复杂的情况。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 15:04:56