Jetpack Compose:TextField装饰器内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
相关产品推荐
相关产品推荐

