如何在Kotlin中实现屏幕旋转时UI状态参数持久化?
卡片表单旋转屏幕未保存数据丢失问题解决
问题描述
基于Kotlin + Jetpack Compose + Dagger Hilt + Room开发的卡片创建/编辑表单,功能基本正常,但未保存记录时旋转屏幕,修改的数据会丢失。例如编辑标题为“X”的卡片,改为“XYZ”后旋转屏幕,标题会变回“X”。使用密封类CardUiState管理屏幕状态。
原因分析
屏幕旋转时,CardScreen中的LaunchedEffect(cardId)会重新执行viewModel.getCard(cardId),该方法会从Room重新加载原始卡片数据,覆盖SavedStateHandle中保存的修改后状态。虽然ViewModel初始化时会从SavedStateHandle恢复状态,但后续执行的getCard会再次请求数据并设置初始状态,导致修改内容丢失。
解决方案
方案一:ViewModel内部优先复用已保存状态
修改ViewModel的getCard方法,先检查SavedStateHandle中是否有已保存的有效状态,有则直接使用,不再从Room加载原始数据:
@HiltViewModel class CardViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val cardRepository: CardRepository, private val categoryRepository: CategoryRepository, private val cardCategoryRelRepository: CardCategoryRelRepository, @ApplicationContext private val context: Context ): ViewModel() { private val _uiState = MutableStateFlow<CardUiState>(CardUiState.Loading) val uiState: StateFlow<CardUiState> = _uiState.asStateFlow() init { val savedState = savedStateHandle.get<CardUiState>("uiState") if (savedState != null) { _uiState.value = savedState } } private fun setState(uiState: CardUiState) { _uiState.value = uiState savedStateHandle["uiState"] = uiState } fun getCard(cardId: Int?) { viewModelScope.launch { // 优先使用已保存的有效状态,避免重新加载原始数据 val savedState = savedStateHandle.get<CardUiState>("uiState") if (savedState != null && savedState !is CardUiState.Loading && savedState !is CardUiState.Error) { _uiState.value = savedState return@launch } try { setState(CardUiState.Loading) val categories = categoryRepository.getCategories().first() if (cardId != null) { val cardWithCategories = cardRepository.getCardWithCategories(cardId).first() setState(CardUiState.Edition( card = cardWithCategories.card, selectedMainCategories = cardWithCategories.categories.filter { it.type == "main" }, mainCategories = categories.filter { it.type == "main" }, selectedTimeCategories = cardWithCategories.categories.filter { it.type == "time" }, timeCategories = categories.filter { it.type == "time" }, )) } else { setState(CardUiState.Creation( mainCategories = categories.filter { it.type == "main" }, timeCategories = categories.filter { it.type == "time" }, )) } } catch (e: Exception) { setState(CardUiState.Error( e.message ?: context.getString(R.string.msg_unknown_error) )) } } } // 其他方法保持不变 fun onChangeTitle(title: String) { val currentState = _uiState.value if (currentState is CardUiState.Creation) { setState(currentState.copy( title = title, error = false, errorMessageResId = null, )) } else if (currentState is CardUiState.Edition) { setState(currentState.copy( title = title, error = false, errorMessageResId = null, )) } } }
方案二:限制LaunchedEffect仅首次执行
调整CardScreen中的LaunchedEffect触发条件,用remember标记初始化状态,确保仅在首次进入页面时调用getCard:
@Composable fun CardScreen( modifier: Modifier = Modifier, titleResId: Int, viewModel: CardViewModel = hiltViewModel(), canNavigateBack: Boolean = false, navigateUp: () -> Unit, cardId: Int? = null, ) { // 标记是否已完成初始化,避免旋转时重复调用getCard val isInitialized = remember { mutableStateOf(false) } LaunchedEffect(cardId) { if (!isInitialized.value) { viewModel.getCard(cardId) isInitialized.value = true } } // 其他代码保持不变 val uiState by viewModel.uiState.collectAsState() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() CardScreenContent( modifier = modifier, titleResId = titleResId, uiState = uiState, scrollBehavior = scrollBehavior, canNavigateBack = canNavigateBack, navigateUp = navigateUp, onTitleChange = { viewModel.onChangeTitle(it) }, onTitlePrefixChange = { viewModel.onChangeTitlePrefix(it) }, onTitleSuffixChange = { viewModel.onChangeTitleSuffix(it) }, onCategoryAdd = { category -> viewModel.addCategoryToSelection(category) }, onCategoryRemove = { category -> viewModel.removeCategoryFromSelection(category) }, onSave = { if (uiState is CardUiState.Creation) { val selectedCategories = ( uiState as CardUiState.Creation ).selectedMainCategories.plus(( uiState as CardUiState.Creation ).selectedTimeCategories) viewModel.save( title = (uiState as CardUiState.Creation).title, titlePrefix = (uiState as CardUiState.Creation).titlePrefix, titleSuffix = (uiState as CardUiState.Creation).titleSuffix, selectedCategories = selectedCategories, ) } else if (uiState is CardUiState.Edition) { val selectedCategories = ( uiState as CardUiState.Edition ).selectedMainCategories.plus(( uiState as CardUiState.Edition ).selectedTimeCategories) viewModel.save( title = (uiState as CardUiState.Edition).title, titlePrefix = (uiState as CardUiState.Edition).titlePrefix, titleSuffix = (uiState as CardUiState.Edition).titleSuffix, selectedCategories = selectedCategories, ) } }, ) }
推荐使用方案一,ViewModel内部处理状态恢复逻辑更可靠,能避免外部触发逻辑的干扰。
内容的提问来源于stack exchange,提问作者forvas
相关产品推荐
相关产品推荐

