本文依据 Jetpack Compose 结构,采用 MVI 架构完结了一个简略的贪吃蛇游戏,展现了 MVI 在 Jetpack Compose 中的方式,并依据 CompositionLocal 完结了简略的换肤功用(可保存至本地)
点此下载 demo:app-debug.apk
运转效果
环境
- Gradle 8.0,这需求 Java17 及以上版别
- Jetpack Compose BOM: 2023.03.00,我之后也会写一篇文章介绍这个版别的更新内容
- Compose 编译器版别:1.4.0
什么是 MVI
MVI 是 Model-View-Intent 的缩写,是一种架构形式,它的中心思维是将 UI 的状态笼统为一个单一的数据流,这个数据流由 View 发出的 Intent 作为输入,经过 Model 处理后,再由 View 显示出来。 详细到本项目,View 是贪吃蛇的游戏界面,Model 是游戏的逻辑,Intent 是用户和体系的操作,比方开始游戏、更改方向等。
- View层:依据 Compose 打造,一切 UI 元素都由代码完结
- Model层:ViewModel 保护 State 的改动,游戏逻辑交由 reduce 处理
- V-M通讯:经过 State 驱动 Compose 改写,工作由 Action 分发至 ViewModel
ViewModel 根本结构如下:
class SnakeGameViewModel : ViewModel() {
// snakeState,UI 观察它的改动来展现不同的画面
val snakeState = mutableStateOf(
SnakeState(
snake = INITIAL_SNAKE,
size = 400 to 400,
blockSize = Size(20f, 20f),
food = generateFood(INITIAL_SNAKE.body)
)
)
// 分发 GameAction
fun dispatch(gameAction: GameAction) {
snakeState.value = reduce(snakeState.value, gameAction)
}
// 依据不同的 gameAction 做不同的处理,并回来新的 snakeState(经过 copy)
private fun reduce(state: SnakeState, gameAction: GameAction): SnakeState {
val snake = state.snake
return when (gameAction) {
GameAction.GameTick -> state.copy(/*...*/)
GameAction.StartGame -> state.copy(gameState = GameState.PLAYING)
// ...
}
}
}
完好代码见:SnakeGameViewModel.kt
UI
因为代码的逻辑均交给了 ViewModel,所以 UI 层的代码量十分少,只需求关注 UI 的展现即可。
@Composable
fun ColumnScope.Playing(
snakeState: SnakeState,
snakeAssets: SnakeAssets,
dispatchAction: (GameAction) -> Unit
) {
Canvas(
modifier = Modifier
.fillMaxSize()
.square()
.onGloballyPositioned {
val size = it.size
dispatchAction(GameAction.ChangeSize(size.width to size.height))
}
.detectDirectionalMove {
dispatchAction(GameAction.MoveSnake(it))
}
) {
drawBackgroundGrid(snakeState, snakeAssets)
drawSnake(snakeState, snakeAssets)
drawFood(snakeState, snakeAssets)
}
}
上面的代码运用 Canvas
作为画布,经过自界说的 square
修饰符使其长宽相等,经过 drawBackgroundGrid
、drawSnake
、drawFood
绘制游戏的背景、蛇和食物。
完好代码见:SnakeGame.kt
主题
本项目自带了一个简略的主题示例,设置不同的主题能够更改蛇的色彩、食物的色彩等
看起来差异不大,但是主要目的在于演示 CompositionLocal 的根本用法
主题功用的完结依据 CompositionLocal
,详细介绍能够参阅 官方文档:运用 CompositionLocal 将数据的效果域限定在局部 。简略来说,父 Composable 运用它,一切子 Composable 中都能获取到对应值,咱们所熟悉的 MaterialTheme
便是经过它完结的。
详细完结如下:
界说类
咱们先界说一个密闭类,表示咱们的主题
sealed class SnakeAssets(
val foodColor: Color= MaterialColors.Orange700,
val lineColor: Color= Color.LightGray.copy(alpha = 0.8f),
val headColor: Color= MaterialColors.Red700,
val bodyColor: Color= MaterialColors.Blue200
) {
object SnakeAssets1: SnakeAssets()
object SnakeAssets2: SnakeAssets(
foodColor = MaterialColors.Purple700,
lineColor = MaterialColors.Brown200.copy(alpha = 0.8f),
headColor = MaterialColors.Blue700,
bodyColor = MaterialColors.Pink300
)
}
上面的 MaterialColors
来自库 FunnySaltyFish/CMaterialColors: 在 Jetpack Compose 中运用 Material Design Color
运用
咱们需求先界说一个 ProvidableCompositionLocal
,在这里,因为主题的变化频率相对较低,因而选用 staticCompositionLocalOf
。之后,在 SnakeGame
外面经过 provide
中缀函数指定咱们的 Assets
internal val LocalSnakeAssets: ProvidableCompositionLocal<SnakeAssets> = staticCompositionLocalOf { SnakeAssets.SnakeAssets1 }
// ....
val snakeAssets by ThemeConfig.savedSnakeAssets
CompositionLocalProvider(LocalSnakeAssets provides snakeAssets) {
SnakeGame()
}
只需求改动 ThemeConfig.savedSnakeAssets
的值,即可全局更改主题款式啦
保存装备到本地(耐久化)
咱们希望用户选择的主题能在下一次翻开应用时依然收效,因而能够把它保存到本地。这里借助的是开源库 FunnySaltyFish/ComposeDataSaver: 在Jetpack Compose中优雅完结数据耐久化。经过它,能够用类似于 rememberState
的方式轻松做到这一点
结构自带了关于根本数据类型的支持,不过因为要保存 SnakeAssets
这个自界说类型,咱们需求提前注册下类型转换器。
class App: Application() {
override fun onCreate() {
super.onCreate()
DataSaverUtils = DataSaverPreferences(this)
// SnakeAssets 使咱们自界说的类型,因而先注册一下转换器,能让它保存时主动转化为 String,读取时主动从 String 恢复成 SnakeAssets
DataSaverConverter.registerTypeConverters(save = SnakeAssets.Saver, restore = SnakeAssets.Restorer)
}
companion object {
lateinit var DataSaverUtils: DataSaverInterface
}
}
然后在 ThemeConfig
中创立一个 DataSaverState
即可
val savedSnakeAssets: MutableState<SnakeAssets> = mutableDataSaverStateOf(DataSaverUtils ,key = "saved_snake_assets", initialValue = SnakeAssets.SnakeAssets1)
之后,对 savedSnakeAssets
的赋值都会主动触发 异步的耐久化操作
,下次翻开应用时也会主动读取。
其他
其实这个项目最早创立于 2022-02 ,作为学习 Compose MVI 的项目。但鉴于当时对 Compose 不那么熟练,写着写着抛弃了;直到 2023-03-31,我在收拾 FunnySaltyFish/JetpackComposeStudy: 本人 Jetpack Compose 主题文章所包含的示例,包含自界说布局、部分组件用法等 时又翻到了这个被我遗忘多时的老项目,于是一时兴起,花了两三个小时把它完结了,并写下了这个 README。或许对后人有些参阅?
项目还附带了一份 Python 的 Pygame 完结的版别,见 python_version
文件夹,运转 main.py
即可
还有一点有趣的工作,当我把 AS 升级到 F(火烈鸟)RC 版别时,发现新建项目时,现已把 Material3 的 Compose 模块放到了第一位了。Google 官方关于推广 Jetpack Compose 的态度,看起来仍是很高涨的。或许这样看来,Compose 仍是有必要学一学的;毕竟即使是 ChatGPT,因为练习集只到 21 年,在写 Compose 的代码上表现也还不尽善尽美呢。(当然,关于下一代、下下一代来说,或许这都不是问题了。但至少不是现在。)
源码
- github.com/FunnySaltyF…
参阅
- 爷童回!Compose + MVI 打造经典版的俄罗斯方块 – ()
- 100 行写一个 Compose 版华容道 – ()
额定感谢
Copilot、ChatGPT
本文正在参与「金石计划」