如何在 Android 中使用密封类 UiState 遵循 MVVM 模式(Kotlin + Jetpack Compose)?

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

我用 Kotlin + Room + Hilt + MVVM 等制作了一个 Android 应用程序。我制作了一个可以创建或编辑播放器的屏幕,为此我制作了可组合项、视图模型和密封类代表 UiState。

它工作正常,但我有两个变量(namealias),在可组合项中带有

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))
            }
        }
    }
}
android kotlin mvvm android-jetpack-compose dagger-hilt
1个回答
0
投票

这样就可以了。

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 值的单一事实来源。因此,它必须保留在可组合项中,因为您已经拥有它了。


未来看起来有点不同:从 Compose 版本 1.7.0 开始,出现了一个新的

BasicTextField

 变体(
TextField
OutlinedTextField
 是构建在 
BasicTextField
 之上的 Material 3 风格)。这个新变体采用了 
TextFieldState
 来弥补其他变体的一些限制。 
鼓励开发人员从使用 String
TextFieldValue
 的变体切换到此变体。但这对于 
TextField
OutlinedTextField
 来说还没有完成,所以你必须等待一段时间才能用 
rememberSaveable { mutableStateOf() }
 替换你的 
rememberTextFieldState()

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