在视图模型中使用密封类 UiState 时如何遵循 MVVM 模式来存储 TextField 的值?

问题描述 投票: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.