我制作了一个可以创建或编辑卡片的表单。我正在使用 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,
)
...
因此,正如我们在评论中已经确定的那样,问题是从
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
负责。