Jetpack Compose for Desktop-004 快速开发井字棋
快速入水
要学习一样跟程序设计有关的东西,最好的办法始终是把手打湿,整一个能够运行,可以实验的东西出来。
也只有在程序开发中,我们才能想一个魔法师而不是魔术师,我们真的能够创造一个东西。而且编译器不会因为我们很丑、我们学历不行、我们没女朋友、我们很穷而拒绝我们,只要我们严格按照手册,就真的可以梦想成真。真是幸运啊!
所以,我们先来写一个简单的程序,一个大家都完全不会感兴趣的游戏:井字棋。
这个游戏实在是无聊,我从来没有成功说服任何一个人跟我一起好好玩过……不过……无聊也是它的好处,马上就行。
构建工具链
现代化的程序设计语言通常都有完整的工具链,负责完成:
- 管理工程-文件的组织
- 编译源代码
- 运行、运行、分发程序
热门但是无法流行的伟大Rust语言,它的工具链是cargo
。
不热门但是很流行的C/C++语言,它的工具链是make
,现在也有cmake
,或者ninja
。
热门流行的Java语言,它的工具链是ant
、maven
和gradle
。
Kotlin是基于Java的,它的工具链我们一般选择gradle
。实际上gradle
、maven
、ant
都是Java的构建工具。
用gradle
构建Kotlin程序,最好玩的是,我们可以编写kotlin
来完成。当然,以前还用groovy
,现在大概可能或许提倡用kotlin
。
构成一个gradle
工程的文件有:
settings.gradle.kts
:工程的设置build.gradle.kts
:工程的构建src
:源代码目录gradle.properties
:gradle的属性文件gradlew
和gradlew.bat
:gradle的wrapper的启动脚本gradle/wrapper
目录:gradle的配置文件和wrappergradle-wrapper.jar
gradle-wrapper.properties
这是在IDEA中一个典型工程的结构:
这里展示了全部的文件,包括隐藏文件:
.gradle
:gradle的缓存目录.idea
:IDEA的配置目录build
:gradle的构建目录.kotlin
:kotlin的配置目录.run
:运行配置目录
有些时候,还有:
out
:编译输出目录
当然,这些都不需要自己来创建,只需要在IDEA中创建一个新的gradle
工程就行了。甚至,下面大部分配置都不需要自己来创建,只需要安装一个Jetpack Compose的插件,然后新建一个Compose工程就行了。
但是,我们这里还是絮絮叨叨一下,可以大概知道这些都是什么。
Gradle Wrapper
为了让工程更加有移植性,我们一般会使用gradle wrapper
,这个工具会自动下载指定版本的gradle
,并且把gradle
的启动脚本放在工程的根目录下。
我们可以通过gradlew
或者gradlew.bat
来启动gradle
,这样就不需要在系统中安装gradle
了。
这相关的几个文件,我们可以通过gradle wrapper
命令来生成。
1gradle wrapper
这个命令会生成gradlew
、gradlew.bat
、gradle/wrapper/gradle-wrapper.jar
、gradle/wrapper/gradle-wrapper.properties
这几个文件。
在最后那个文件中,通常包含有下载的地址和版本号。
1distributionBase=GRADLE_USER_HOME
2distributionPath=wrapper/dists
3distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip
4zipStoreBase=GRADLE_USER_HOME
5zipStorePath=wrapper/dists
我们通(bi)常(xu)更改那个distributionUrl
地址,指向我们自己的镜像源,这样下载会快一些。
gradle.properties
这个文件通常用来存放一些工程的属性,比如版本号、插件版本等等。
1org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2kotlin.code.style=official
3kotlin.version=2.0.0
4compose.version=1.6.10
第一行的org.gradle.jvmargs
是gradle
的启动参数,这里设置了堆内存和文件编码。
在调用gradle
的时候,我们可以通过-P
参数来传递这些属性,比如:
1gradle build -Pkotlin.version=1.5.31
这里设定的属性,可以在build.gradle.kts
文件中使用。
settings.gradle.kts
我们需要在settings.gradle.kts
文件中添加一些内容,这个文件是一个Kotlin脚本,用来描述工程的设置。
1pluginManagement {
2 repositories {
3 maven("https://maven.aliyun.com/repository/public")
4 maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
5 google()
6 gradlePluginPortal()
7 mavenCentral()
8 }
9
10 plugins {
11 kotlin("jvm").version(extra["kotlin.version"] as String)
12 id("org.jetbrains.compose").version(extra["compose.version"] as String)
13 id("org.jetbrains.kotlin.plugin.compose").version(extra["kotlin.version"] as String)
14 }
15}
16
17rootProject.name = "Demo004"
这个文件首先描述了插件的管理,然后指定了工程的名称。
当然,我们也在这里添加了一些镜像源,这样下载插件会快一些。这个技能必须要掌握……不掌握就会觉得体验极其糟糕……什么都打不开,什么都运行不了。
build.gradle.kts
我们需要在build.gradle.kts
文件中添加一些内容,这个文件是一个Kotlin脚本,用来描述工程的构建过程。
1import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2import org.jetbrains.kotlin.gradle.targets.js.npm.fromSrcPackageJson
3
4plugins {
5 kotlin("jvm")
6 id("org.jetbrains.compose")
7 id("org.jetbrains.kotlin.plugin.compose")
8}
9
10group = "org.cardc.fdii"
11version = "1.0.0"
12
13repositories {
14 maven("https://maven.aliyun.com/repository/public")
15 mavenCentral()
16 maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
17 google()
18}
19
20dependencies {
21 // Note, if you develop a library, you should use compose.desktop.common.
22 // compose.desktop.currentOs should be used in launcher-sourceSet
23 // (in a separate module for demo project and in testMain).
24 // With compose.desktop.common you will also lose @Preview functionality
25 implementation(compose.desktop.currentOs)
26
27 // Gson dependency
28 implementation("com.google.code.gson:gson:2.11.0")
29}
30
31compose.desktop {
32 application {
33 mainClass = "MainKt"
34
35 nativeDistributions {
36 targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
37 packageName = "Demo004"
38 packageVersion = "1.0.0"
39 }
40 }
41}
这里面,通常只更改了几处:
group
:工程的组织version
:工程的版本,通常我会从1.0-SNAPSHOT
改成1.0.0
,前者在分发的时候会有问题repositories
:镜像源,这里添加了一些镜像源,同样!这个技能必须要掌握!dependencies
:依赖,这里compose.desktop.currentOs
,这是Compose Desktop的依赖,如果是Compose工程,自动就有;后面的gson
是一个JSON库,我们可能会用到。
src
目录
这个目录是源代码目录,我们可以在这个目录下创建kotlin
、java
、resources
等等目录,用来存放源代码和资源文件。
当然,以这个工程为例,我们只需要在src/main/kotlin
目录下创建一个Main.kt
文件,这个文件就是程序的入口。
1import androidx.compose.desktop.ui.tooling.preview.Preview
2import androidx.compose.foundation.background
3import androidx.compose.foundation.layout.*
4import androidx.compose.material.Button
5import androidx.compose.material.ButtonDefaults
6import androidx.compose.material.Text
7import androidx.compose.runtime.*
8import androidx.compose.ui.Alignment
9import androidx.compose.ui.Modifier
10import androidx.compose.ui.graphics.Color
11import androidx.compose.ui.res.useResource
12import androidx.compose.ui.text.style.TextAlign
13import androidx.compose.ui.unit.DpSize
14import androidx.compose.ui.unit.dp
15import androidx.compose.ui.unit.sp
16import androidx.compose.ui.window.Window
17import androidx.compose.ui.window.WindowPosition
18import androidx.compose.ui.window.application
19import androidx.compose.ui.window.rememberWindowState
20import java.io.File
21
22@Composable
23fun TicTocTile(
24 x: Int,
25 y: Int,
26 ticTacToe: TicTacToe
27) {
28
29 Box(
30 modifier = Modifier
31 .size(100.dp)
32 .background(Color.LightGray),
33 contentAlignment = Alignment.Center
34 ) {
35
36 Button(modifier = Modifier
37 .padding(5.dp)
38 .fillMaxSize(),
39 colors = ButtonDefaults.buttonColors(backgroundColor = ticTacToe[x, y].color()),
40 onClick = {
41 if (ticTacToe.isGameOver()) {
42 ticTacToe.startNewGame()
43 return@Button
44 }
45
46 ticTacToe.nextMove(x, y)
47
48 }) {
49 Text(
50 text = ticTacToe.textAt(x, y),
51 fontSize = 36.sp,
52 color = Color.Black
53 )
54 }
55 }
56}
57
58
59@Composable
60@Preview
61fun TicToc() {
62 val board = remember {
63 mutableStateMapOf<Pair<Int, Int>, Player>(
64 Pair(0, 0) to Player.NULL,
65 Pair(0, 1) to Player.NULL,
66 Pair(0, 2) to Player.NULL,
67 Pair(1, 0) to Player.NULL,
68 Pair(1, 1) to Player.NULL,
69 Pair(1, 2) to Player.NULL,
70 Pair(2, 0) to Player.NULL,
71 Pair(2, 1) to Player.NULL,
72 Pair(2, 2) to Player.NULL
73 )
74 }
75 val player = remember { mutableStateOf(Player.X) }
76
77 val ticTacToe = TicTacToe(board, player)
78
79 Column {
80 for (i in 0..2) {
81 Row {
82 for (j in 0..2) {
83 TicTocTile(i, j, ticTacToe)
84 }
85 }
86 }
87 Text(
88 modifier = Modifier.padding(10.dp).align(Alignment.CenterHorizontally),
89 text = ticTacToe.gameText(),
90 fontSize = 30.sp,
91 color = ticTacToe.color(),
92 textAlign = TextAlign.Center
93 )
94
95 }
96
97}
98
99fun main() = application {
100 useResource("config.json") {
101 config = loadConfig(it)
102 }
103
104 Window(
105 onCloseRequest = ::exitApplication,
106 title = "Tic Tac Toe",
107 state = rememberWindowState(
108 position = WindowPosition.Aligned(Alignment.Center),
109 size = DpSize(320.dp, 400.dp)
110 )
111 ) {
112 Box(
113 modifier = Modifier.background(Color.White).fillMaxSize(),
114 contentAlignment = Alignment.Center
115 ) {
116 TicToc()
117 }
118
119 }
120}
然后是TicTacToe
的实现:
1enum class Player {
2 NULL, X, O;
3
4 fun nameString(): String {
5 return when (this) {
6 X -> "X"
7 O -> "O"
8 else -> ""
9 }
10 }
11
12
13}
14
15import androidx.compose.runtime.MutableState
16import androidx.compose.runtime.snapshots.SnapshotStateMap
17
18
19data class TicTacToe(val board: SnapshotStateMap<Pair<Int, Int>, Player>, val player: MutableState<Player>) {
20 fun gameText(): String {
21 val winner = winner()
22 if (isGameOver()) {
23 if (winner == Player.NULL) {
24 return "Game Over Tie."
25 }
26 return "Player ${winner.nameString()} won!"
27 }
28 return "Player ${player.value.nameString()}'s turn"
29 }
30
31
32 fun winner(): Player {
33 for (i in 0..2) {
34 if (get(i, 0) != Player.NULL && get(i, 0) == get(i, 1) && get(i, 1) == get(i, 2)) {
35 return get(i, 0)
36 }
37 if (get(0, i) != Player.NULL && get(0, i) == get(1, i) && get(1, i) == get(2, i)) {
38 return get(0, i)
39 }
40 }
41 if (get(0, 0) != Player.NULL && get(0, 0) == get(1, 1) && get(1, 1) == get(2, 2)) {
42 return get(0, 0)
43 }
44 if (get(0, 2) != Player.NULL && get(0, 2) == get(1, 1) && get(1, 1) == get(2, 0)) {
45 return get(0, 2)
46 }
47 return Player.NULL
48 }
49
50
51 fun nextMove(x: Int, y: Int) {
52 if (isTaking(x, y)) return
53 board.put(Pair(x, y), player.value)
54 nextPlayer()
55 }
56
57 operator fun get(x: Int, y: Int): Player {
58 return board[Pair(x, y)] ?: Player.NULL
59 }
60
61 fun textAt(x: Int, y: Int): String {
62 return get(x, y).nameString()
63 }
64
65 fun startNewGame() {
66 for (key in board.keys) {
67 this.board.put(key, Player.NULL)
68 }
69 player.value = Player.X
70 }
71
72 fun isGameOver(): Boolean {
73 return winner() != Player.NULL || isFilled()
74 }
75
76 private fun isFilled(): Boolean {
77 board.values.find { it == Player.NULL }?.let {
78 return false
79 }
80 return true
81 }
82
83 private fun isTaking(x: Int, y: Int): Boolean {
84 return get(x, y) != Player.NULL
85 }
86
87 private fun nextPlayer() {
88 player.value = if (player.value == Player.X) Player.O else Player.X
89 }
90
91
92}
为了配合显示颜色,我们还需要一个扩展函数:
1import Player.O
2import Player.X
3import androidx.compose.ui.graphics.Color
4import com.google.gson.Gson
5import java.io.File
6import java.io.InputStream
7
8
9data class Config(
10 val PlayerXColor: String="00FF00",
11 val PlayerOColor: String="#0000FF",
12 val GameOverColor: String="##FF0000"
13)
14
15fun loadConfig(file: InputStream): Config {
16
17 try {
18 val json = file.readAllBytes().decodeToString()
19 return Gson().fromJson(json, Config::class.java)
20 } catch (e: Exception) {
21 e.printStackTrace()
22 return Config("#FF0000", "#0000FF", "#00FF00")
23 }
24}
25
26val String.color
27 get() = Color(removePrefix("#").toInt(16)).copy(alpha = 1f)
28
29lateinit var config: Config
30
31fun Player.color(): Color {
32 return when (this) {
33 X -> config.PlayerXColor.color
34 O -> config.PlayerOColor.color
35 else -> Color.White
36 }
37}
38
39fun TicTacToe.color(): Color {
40 if (isGameOver()) {
41 return config.GameOverColor.color
42 }
43 return player.value.color()
44}
这个负责从config.json
文件中读取颜色配置,然后在游戏中对不同玩家显示不同颜色,并给出游戏结束时的颜色。
这个config.json
文件的内容是:
1{
2 "PlayerXColor": "#00FF00",
3 "PlayerOColor": "#0000FF",
4 "GameOverColor": "#FF0000"
5}
这样,我们就完成了一个简单的井字棋游戏。
运行
在windows下面,我们可以通过gradlew.bat
来运行这个程序:
1./gradlew.bat run
还能够通过gradle
来构建这个程序:
1./gradlew.bat createDistributable
这样就会在build/compose/binaries
目录下生成一个可执行文件程序文件夹,里面包含了可执行文件和依赖库。
这个文件拷贝到其他地方,就可以运行了。
当然在IDEA中——默认你使用的是IDEA——你可以直接右边的Gradle
工具栏中选择Tasks
,然后选择application
,然后选择run
,就可以运行这个程序。
总结
这个程序是一个简单的井字棋游戏,我们通过Jetpack Compose来实现了界面,通过Kotlin来实现了逻辑。
至于程序的实现细节,源代码的解读,就放在下次。
文章标签
|-->jetpack |-->compose-desktop |-->kotlin
- 本站总访问量:次
- 本站总访客数:人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository