屏幕旋转时SnackBar重复弹出问题求助(RegistrationUiState.Error)
问题分析
你的问题根源在于:屏幕旋转会触发Composable重组,而RegistrationUiState.Error是一个持久化的状态(ViewModel在配置变更后仍保留该状态),导致每次重组时LaunchedEffect都会重新执行,从而重复弹出SnackBar。
解决方案
方案一:使用ViewModel一次性事件(推荐)
这是Jetpack Compose处理一次性UI事件(如弹出SnackBar、导航)的标准做法,避免状态和事件混淆。
步骤1:在ViewModel中添加事件流
// 定义一次性事件密封类 sealed class RegistrationEvent { object ShowPasswordUpdateError : RegistrationEvent() object ShowPasswordUpdateSuccess : RegistrationEvent() } class UpdatePasswordViewModel : ViewModel() { private val _updatePasswordUiState = MutableStateFlow<RegistrationUiState>(RegistrationUiState.Initial) val updatePasswordUiState = _updatePasswordUiState.asStateFlow() // 用于发送一次性事件的SharedFlow private val _events = MutableSharedFlow<RegistrationEvent>() val events = _events.asSharedFlow() // 密码更新失败时调用 private fun onUpdateFailed() { viewModelScope.launch { _events.emit(RegistrationEvent.ShowPasswordUpdateError) } // 重置状态为Initial,让表单可再次操作 _updatePasswordUiState.value = RegistrationUiState.Initial } // 密码更新成功时调用 private fun onUpdateSuccess() { viewModelScope.launch { _events.emit(RegistrationEvent.ShowPasswordUpdateSuccess) } _updatePasswordUiState.value = RegistrationUiState.Success } }
步骤2:在Composable中收集事件并处理
@Composable fun UpdatePasswordScreen( viewModel: UpdatePasswordViewModel = hiltViewModel(), navigateBack: () -> Unit ) { val uiState by viewModel.updatePasswordUiState.collectAsStateWithLifecycle() val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } // 收集一次性事件 LaunchedEffect(viewModel) { viewModel.events.collect { event -> when (event) { RegistrationEvent.ShowPasswordUpdateError -> { snackbarHostState.showSnackbar( context.getString(R.string.could_not_update_password_error), context.getString(R.string.dialog_action_ok), duration = SnackbarDuration.Short ) } RegistrationEvent.ShowPasswordUpdateSuccess -> { snackbarHostState.showSnackbar( context.getString(R.string.password_changed_successfully), context.getString(R.string.dialog_action_ok), duration = SnackbarDuration.Short ) navigateBack() } } } } Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { MifosTopBar( title = { Text(text = stringResource(id = R.string.change_password)) }, navigateBack = navigateBack ) } ) { padding -> Box(modifier = Modifier.padding(padding)) { UpdatePasswordContent() when (uiState) { is RegistrationUiState.Loading -> { MifosProgressIndicator( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background.copy(0.8f)) ) } else -> Unit // Error和Success通过事件处理 } } } }
方案二:仅修改Composable(无需改动ViewModel)
如果暂时不想修改ViewModel结构,可以通过在Composable中跟踪已显示的错误来避免重复弹出:
@Composable fun UpdatePasswordScreen( uiState: RegistrationUiState, navigateBack: () -> Unit ) { val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } // 保存最后一次显示过SnackBar的错误状态 val lastShownError = rememberSaveable { mutableStateOf<RegistrationUiState.Error?>(null) } Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { MifosTopBar( title = { Text(text = stringResource(id = R.string.change_password)) }, navigateBack = navigateBack ) } ) { padding -> Box(modifier = Modifier.padding(padding)) { UpdatePasswordContent() when (uiState) { is RegistrationUiState.Loading -> { MifosProgressIndicator( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background.copy(0.8f)) ) } is RegistrationUiState.Error -> { // 仅当是新错误时才显示SnackBar if (lastShownError.value != uiState) { LaunchedEffect(snackbarHostState, uiState) { snackbarHostState.showSnackbar( context.getString(R.string.could_not_update_password_error), context.getString(R.string.dialog_action_ok), duration = SnackbarDuration.Short ) lastShownError.value = uiState } } } is RegistrationUiState.Initial -> { // 回到初始状态时重置跟踪的错误 lastShownError.value = null } is RegistrationUiState.Success -> { LaunchedEffect(snackbarHostState) { snackbarHostState.showSnackbar( context.getString(R.string.password_changed_successfully), context.getString(R.string.dialog_action_ok), duration = SnackbarDuration.Short ) } navigateBack.invoke() } } } } }
注意事项
- 方案二要求
RegistrationUiState.Error是data class,这样才能通过对象相等性判断是否为新错误。 - 方案一更符合MVVM架构设计原则,将状态和事件分离,避免持久化状态触发重复操作。
内容的提问来源于stack exchange,提问作者Pronay Sarker
相关产品推荐
相关产品推荐

