我用 Kotlin + Room + Hilt + MVVM 等制作了一个 Android 应用程序。我制作了一个可以创建或编辑播放器的屏幕,为此我制作了可组合项、视图模型和密封类代表 UiState。
它工作正常,但我有两个变量(name和alias),在可组合项中带有
rememberSaveable
,我想它们应该在视图模型中或集成在UiState中。你能帮我吗?我的代码可以吗?或者我应该在 MVVM 移动这些变量后改进它?哪个是最佳实践?
这是代码:
UiState
sealed class PlayerUiState {
data object Loading : PlayerUiState()
data object Creation : PlayerUiState()
data class Edition(val player: Player) : PlayerUiState()
data class Error(val message: String) : PlayerUiState()
}
查看模型
@HiltViewModel
class PlayerViewModel @Inject constructor(
private val playerRepository: PlayerRepository,
@ApplicationContext private val context: Context,
) : ViewModel() {
private val _uiState = MutableStateFlow<PlayerUiState>(PlayerUiState.Loading)
val uiState: StateFlow<PlayerUiState> = _uiState.asStateFlow()
fun setState(uiState: PlayerUiState) {
_uiState.value = uiState
}
fun getPlayer(playerId: Int) {
viewModelScope.launch {
playerRepository.getPlayer(playerId).onStart {
setState(PlayerUiState.Loading)
}.catch { e ->
setState(
PlayerUiState.Error(
e.message ?: context.getString(R.string.msg_unknown_error)
)
)
}.collect { player ->
setState(PlayerUiState.Edition(player))
}
}
}
fun insertPlayer(name: String, alias: String) {
viewModelScope.launch {
playerRepository.insertPlayer(
Player(
name = name,
alias = alias,
)
)
}
}
fun updatePlayer(player: Player) {
viewModelScope.launch {
playerRepository.updatePlayer(player)
}
}
}
可组合项
@Composable
fun PlayerScreen(
modifier: Modifier = Modifier,
titleRes: Int,
viewModel: PlayerViewModel = hiltViewModel(),
playerId: Int? = null,
) {
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(playerId) {
if (playerId != null) {
viewModel.getPlayer(playerId)
} else {
viewModel.setState(PlayerUiState.Creation)
}
}
PlayerScreenContent(
modifier = modifier,
titleRes = titleRes,
uiState = uiState,
onInsert = { name, alias -> viewModel.insertPlayer(name, alias) },
onUpdate = { name, alias ->
val player = (uiState as PlayerUiState.Edition).player
viewModel.updatePlayer(player.copy(name = name, alias = alias))
},
)
}
@Composable
fun PlayerScreenContent(
modifier: Modifier = Modifier,
titleRes: Int,
uiState: PlayerUiState = PlayerUiState.Loading,
onInsert: (String, String) -> Unit,
onUpdate: (String, String) -> Unit,
) {
Scaffold() { innerPadding ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
when (uiState) {
is PlayerUiState.Loading -> CircularProgressIndicator()
is PlayerUiState.Error -> ErrorDialog(
text = uiState.message,
)
is PlayerUiState.Creation -> PlayerForm(
onSave = onInsert,
)
is PlayerUiState.Edition -> PlayerForm(
player = uiState.player,
onSave = onUpdate,
)
}
}
}
}
@Composable
fun PlayerForm(
player: Player? = null,
onSave: (String, String) -> Unit,
) {
var name by rememberSaveable { mutableStateOf(player?.name ?: "") }
var alias by rememberSaveable { mutableStateOf(player?.alias ?: "") }
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 = name,
onValueChange = { name = it },
label = { Text(stringResource(id = R.string.text_field_name)) },
)
OutlinedTextField(
value = alias,
onValueChange = { alias = it },
label = { Text(stringResource(id = R.string.text_field_alias)) },
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.align(Alignment.BottomCenter),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Button(
onClick = { onSave(name, alias) },
enabled = name.isNotEmpty(),
) {
Text(stringResource(id = R.string.button_save))
}
}
}
}
这样就可以了。
TextField 有两种变体,一种使用
String
作为其值,另一种使用 TextFieldValue
。您使用使用 String
的变体,并且该 String 需要由 State 包装,否则 TextField 在更改时不会重新组合。
你已经拥有了:
var name by rememberSaveable { mutableStateOf(player?.name ?: "") }
var alias by rememberSaveable { mutableStateOf(player?.alias ?: "") }
现在,应该将其移至视图模型吗?由于视图模型不应包含任何 MutableState,唯一的选择是将其更改为 MutableStateFlows,以便稍后可以在可组合项中使用
collectAsState()
将它们转换为状态。
但这也是您不应该做的事情:不要使用异步数据结构(MutableStateFlow 就是)来存储 TextField 值。您可以在Compose 中 TextField 的有效状态管理中阅读有关此主题的更多信息。
这让您没有选项来保存视图模型中 TextField 值的单一事实来源。因此,它必须保留在可组合项中,因为您已经拥有它了。
BasicTextField
变体(
TextField
和
OutlinedTextField
是构建在
BasicTextField
之上的 Material 3 风格)。这个新变体采用了
TextFieldState
来弥补其他变体的一些限制。鼓励开发人员从使用
String
和
TextFieldValue
的变体切换到此变体。但这对于 TextField
和
OutlinedTextField
来说还没有完成,所以你必须等待一段时间才能用
rememberSaveable { mutableStateOf(...) }
替换你的
rememberTextFieldState()
。