Kotlin 中如何让 uiState 的参数在转动屏幕时保持不变?

问题描述 投票:0回答:1

我制作了一个可以创建或编辑卡片的表单。我正在使用 Kotlin + Jetpack Compose + Dagger Hilt + Room。它工作正常,除了一件事。当我转动屏幕而不保存记录时,我会丢失修改的数据。示例:如果我正在编辑标题为“X”的卡片,并将其标题更改为“XYZ”,然后转动屏幕,标题将返回到“X”。

你能帮我吗?我正在为国家使用密封课程。顺便说一句,如果您发现代码中的错误或任何可以改进的地方,您可以告诉我,我们将非常感激(因为我不是 Android 专家,我想遵循良好的实践来做到这一点) :

这是屏幕的状态:

@Parcelize
sealed class CardUiState: Parcelable {
    @Parcelize
    data object Loading: CardUiState()
    @Parcelize
    data class Creation(
        val titlePrefix: String = "",
        val title: String = "",
        val titleSuffix: String = "",
        val mainCategories: List<Category> = emptyList(),
        val selectedMainCategories: List<Category> = emptyList(),
        val timeCategories: List<Category> = emptyList(),
        val selectedTimeCategories: List<Category> = emptyList(),
        val error: Boolean = false,
        val errorMessageResId: Int? = null,
    ) : CardUiState()
    @Parcelize
    data class Edition(
        val card: Card,
        val titlePrefix: String = card.titlePrefix,
        val title: String = card.title,
        val titleSuffix: String = card.titleSuffix,
        val mainCategories: List<Category> = emptyList(),
        val selectedMainCategories: List<Category> = emptyList(),
        val timeCategories: List<Category> = emptyList(),
        val selectedTimeCategories: List<Category> = emptyList(),
        val error: Boolean = false,
        val errorMessageResId: Int? = null,
    ): CardUiState()
    @Parcelize
    data class Error(
        val message: String
    ): CardUiState()
}

这是视图模型(它更大,但我只粘贴相关代码):

@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 {
            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,
            ))
        }
    }
...

这是屏幕(它大得多,但我只是粘贴相关代码的和平):

@Composable
fun CardScreen(
    modifier: Modifier = Modifier,
    titleResId: Int,
    viewModel: CardViewModel = hiltViewModel(),
    canNavigateBack: Boolean = false,
    navigateUp: () -> Unit,
    cardId: Int? = null,
) {
    LaunchedEffect(cardId) {
        viewModel.getCard(cardId)
    }
    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,
                )
            }
        },
    )
}

...

    when (uiState) {
        is CardUiState.Loading -> CircularProgressIndicator()
        is CardUiState.Error -> ErrorDialog(
            text = uiState.message,
            onDismiss = navigateUp
        )
        is CardUiState.Creation -> CardForm(
            title = uiState.title,
            titlePrefix = uiState.titlePrefix,
            titleSuffix = uiState.titleSuffix,
            mainCategories = uiState.mainCategories,
            selectedMainCategories = uiState.selectedMainCategories,
            timeCategories = uiState.timeCategories,
            selectedTimeCategories = uiState.selectedTimeCategories,
            onTitleChange = onTitleChange,
            onTitlePrefixChange = onTitlePrefixChange,
            onTitleSuffixChange = onTitleSuffixChange,
            onCategoryAdd = onCategoryAdd,
            onCategoryRemove = onCategoryRemove,
            error = uiState.error,
            errorMessageResId = uiState.errorMessageResId,
        )
        is CardUiState.Edition -> CardForm(
            title = uiState.title,
            titlePrefix = uiState.titlePrefix,
            titleSuffix = uiState.titleSuffix,
            mainCategories = uiState.mainCategories,
            selectedMainCategories = uiState.selectedMainCategories,
            timeCategories = uiState.timeCategories,
            selectedTimeCategories = uiState.selectedTimeCategories,
            onTitleChange = onTitleChange,
            onTitlePrefixChange = onTitlePrefixChange,
            onTitleSuffixChange = onTitleSuffixChange,
            onCategoryAdd = onCategoryAdd,
            onCategoryRemove = onCategoryRemove,
            error = uiState.error,
            errorMessageResId = uiState.errorMessageResId,
        )
    }

...

@Composable
fun CardForm(
    title: String = "",
    titlePrefix: String = "",
    titleSuffix: String = "",
    mainCategories: List<Category> = emptyList(),
    selectedMainCategories: List<Category> = emptyList(),
    timeCategories: List<Category> = emptyList(),
    selectedTimeCategories: List<Category> = emptyList(),
    onTitleChange: (String) -> Unit = {},
    onTitlePrefixChange: (String) -> Unit = {},
    onTitleSuffixChange: (String) -> Unit = {},
    onCategoryAdd: (Category) -> Unit = {},
    onCategoryRemove: (Category) -> Unit = {},
    error: Boolean = false,
    errorMessageResId: Int? = null,
) {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        contentAlignment = Alignment.BottomCenter
    ) {
        Column(
            modifier = Modifier.align(Alignment.Center),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            OutlinedTextField(
                value = titlePrefix,
                onValueChange = onTitlePrefixChange,
                label = { Text(stringResource(id = R.string.text_field_title_prefix)) },
                singleLine = true,
            )

...

kotlin android-jetpack-compose android-room android-viewmodel dagger-hilt
1个回答
0
投票

因此,正如我们在评论中已经确定的那样,问题是从

viewModel.getCard
调用
LaunchedEffect
,当您旋转屏幕时会调用它,并将从存储库重新加载卡片数据并替换
SavedStateHandle
中的数据。

由于您使用 androidx 导航,因此您可以从视图模型内部的

SavedStateHandle
获取导航参数,请参阅 文档。另请注意,您可以方便地直接从 getStateFlow
 
SavedStateHandle
,这样您就不必分别更新两个地方。生成的代码可能如下所示:

class CardViewModel(
  private val savedStateHandle: SavedStateHandle,
) : ViewModel() {

  val uiState = savedStateHandle.getStateFlow<CardUiState>("uiState", CardUiState.Loading)

  init {
    if (uiState.value == CardUiState.Loading) {
      // CardUiState was not saved in savedStateHandle, you have to load data
      viewModelScope.launch {
        // get the id from you navigation arguments
        val cardId = savedStateHandle.toRoute<YourRoute>.cardId
        // load data from repository
        setState(getCard(cardId))
      }
    }
  }

  private suspend fun getCard(cardId: Int?): CardUiState {
    // ...
  }

  private fun setState(uiState: CardUiState) {
    savedStateHandle["uiState"] = uiState
  }

  // the rest stays the same:
  fun onChangeTitle(title: String) {
    val currentState = uiState.value
    val newState = ...
    setState(newState)
  }
}

从撰写代码中,您只需观察

uiState
并使用
onChangeTitle
等方法更新数据。加载数据(getCard)由
ViewModel
负责。

© www.soinside.com 2019 - 2024. All rights reserved.