大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说Jetpack Compose + MVI 实现一个简易贪吃蛇,希望您对编程的造诣更进一步.
本文基于 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…
参考
- Jetpack Compose + MVI 实现一个简易贪吃蛇
- Jetpack Compose + MVI 实现一个简易贪吃蛇
额外感谢
Copilot、ChatGPT
本文正在参加「金石计划」
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/36727.html