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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 07:39:53