Compose 中的 ViewModel 和状态

Compose 中的 ViewModel 和状态

记录一下

ViewModel

ViewModel 组件用于存储和公开界面所使用的状态。界面状态是经过 ViewModel 转换的应用数据。ViewModel 可让您的应用遵循通过模型驱动界面的架构原则。

ViewModel 会存储应用相关的数据,这些数据不会在 Android 框架销毁并重新创建 activity 时销毁。与 activity 实例不同,ViewModel 对象不会被销毁。应用会在配置更改期间自动保留 ViewModel 对象,以便它们存储的数据在重组后立即可用。

如需在应用中实现 ViewModel,请扩展架构组件库中提供的 ViewModel 类,并将应用数据存储在该类中。

流程

emmmmmm,画完流程图发现,本文重点不应该是流程图,而是ViewModel使用,即UI动作怎么更新到底层数据的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
s=>start: 按下Submit/键盘按下Done按键
e=>end: 结束,等待下一次
1=>condition: 判断是否正确
11=>operation: 更新错误标志位
12=>operation: 得分+
13=>operation: 更新游戏状态
14=>condition: 检查计数是
否需要结束
141=>operation: 清除失败标志
142=>operation: 得分更新
143=>operation: 标记游戏结束
144=>operation: 清除失败标志
145=>operation: 随机一个新单词
146=>operation: 得分更新
147=>operation: 单词计数+1

s->1(no)->11->e
1(yes)->12->13->14(yes)->141->142->143
14(no)->144->145->146->147->e

代码

文中代码段只做留了数据流相关部分,替换了R.中的资源,不完整

GameViewModel.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class GameViewModel : ViewModel() {
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()

private lateinit var currentWord: String
private var usedWords: MutableSet<String> = mutableSetOf()

var userGuess by mutableStateOf("") // 只有私有设置,公开get
private set

fun updateUserGuess(guessWord: String) { // 更新变量
userGuess = guessWord
}

fun checkUserGuess() { // 检查用户猜测
if (userGuess.equals(currentWord, ignoreCase = true)) { // 比较字符串忽略大小写
val updatedScore = _uiState.value.score.plus(SCORE_INCREASE) // 如果相同,更新得分
updateGameState(updatedScore)
} else {
_uiState.update { currentState ->
currentState.copy(isGuessWordWrong = true) // 更新状态,同时标记猜测错误
}
}
updateUserGuess("") // 清空猜测的单词,会清空输入框
}

private fun updateGameState(updatedScore: Int) { // 更新得分
if (usedWords.size == MAX_NO_OF_WORDS) { // 游戏结束,到达10次
_uiState.update { currentState ->
currentState.copy(
isGuessWordWrong = false,
score = updatedScore, // 更新分数
isGameOver = true // 游戏结束
)
}
} else { // 继续游戏
_uiState.update { currentState ->
currentState.copy(
isGuessWordWrong = false,
currentScrambledWord = pickRandomWordAndShuffle(), // 随机一个单词
score = updatedScore, // 更新得分
currentWordCount = currentState.currentWordCount.inc() // 单词计数+1
)
}
}
}

private fun shuffleCurrentWord(word: String): String { return String("") } // 返回乱序单词

private fun pickRandomWordAndShuffle(): String { return String("") } // 随机单词

fun resetGame() {
usedWords.clear() // 清除记录
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle()) // 重置状态使用新的随机单词
}

fun skipWord() {
updateGameState(_uiState.value.score) // 使用当前分数更新分数,即没有增加
updateUserGuess("") // 清空输入框
}

init {
resetGame()
}
}

GameScreen.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
@Composable
fun GameScreen(
gameViewModel: GameViewModel = viewModel()
) {
val gameUiState by gameViewModel.uiState.collectAsState()

Column {
Text("Unscramble")
GameLayout(
onUserGuessChanged = { // 每输入一个字符就更新变量,并反馈到显示
gameViewModel.updateUserGuess(it)
},
onKeyboardDone = { // 按下键盘确定,检查单词
gameViewModel.checkUserGuess()
},
isGuessWrong = gameUiState.isGuessWordWrong, // 是否错误,用来选择输入框外层提示
userGuess = gameViewModel.userGuess, // 输入的内容,用于在输入框显示
wordCount = gameUiState.currentWordCount, // 单词计数,右上角显示
currentScrambledWord = gameUiState.currentScrambledWord, // 当前随机出来并乱序的单词
)
Column {
Button( // 提交按钮
onClick = { gameViewModel.checkUserGuess() }
) {
Text("Submit")
}

OutlinedButton( // 跳过按钮
onClick = { gameViewModel.skipWord() },
) {
Text("Skip")
}
}
Card {
Text(("Score: %d", gameUiState.score))
}
if (gameUiState.isGameOver){ // 如果结束了,就弹提示框,标志位在 updateGameState 中置位
FinalScoreDialog(
score = gameUiState.score,
onPlayAgain = {
gameViewModel.resetGame()
}
)
}
}
}

@Composable
fun GameLayout(
onUserGuessChanged: (String) -> Unit,
isGuessWrong: Boolean,
userGuess: String,
onKeyboardDone: () -> Unit,
wordCount: Int,
currentScrambledWord: String,
) {
Card {
Column {
Text(("%d/ 10", wordCount))
Text(currentScrambledWord)
Text("Unscramble the word using all the letters.")
OutlinedTextField(
value = userGuess, // 输入的内容
onValueChange = onUserGuessChanged,
label = { // 是否错误
if(!isGuessWrong) {
Text("Enter your word")
} else {
Text("Wrong Guess!")
}
},
isError = isGuessWrong, // 颜色
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { onKeyboardDone() }
)
)
}
}
}

@Composable
private fun FinalScoreDialog( // 到达10次后的弹窗,选择重新开始或者退出
score: Int, // 显示的分数
onPlayAgain: () -> Unit, // 点击继续
) {
val activity = (LocalContext.current as Activity)

AlertDialog(
title = { Text("Congratulations!") },
text = { Text(("You scored: %d", score)) },
dismissButton = {
TextButton(
onClick = {
activity.finish()
}
) {
Text("Exit")
}
},
confirmButton = {
TextButton(
onClick = onPlayAgain
) {
Text("Play Again")
}
}
)
}

GameUiState.kt

1
2
3
4
5
6
7
8
9
package com.example.unscramble.ui

data class GameUiState( // 状态变量
val currentScrambledWord: String = "",
val isGuessWordWrong: Boolean = false,
val score: Int = 0,
val currentWordCount: Int = 1,
val isGameOver: Boolean = false
)