第一个KMP应用

This commit is contained in:
SOR
2026-09-18 10:26:33 +08:00
parent 1c55a400e1
commit 4d9c28a86a
4 changed files with 224 additions and 5 deletions
@@ -0,0 +1 @@
# 复杂逻辑与多平台不同逻辑
+2
View File
@@ -79,6 +79,8 @@ class MyClass(
## 特殊函数
### 嵌套 lambda 与注解
### infix 函数
### 扩展函数
+215 -1
View File
@@ -1 +1,215 @@
# 第一个KMP应用
# 第一个 KMP 应用
这里,我推荐刚开始上手 KMP 的朋友们先写仅有 JVM 平台的 KMP 应用。
## 项目结构
- 项目根目录/
- desktopApp/
- src/
- build.gradle.kts
- shared/
- src/
- commonMain/
- composeResources/
- kotlin/
- commonTest/
- jvmMain/
- build.gradle.kts
- build.gradle.kts
- settings.gradle.kts
## 跑起来看看效果
Gradle 默认会注册启动项 `hotRun`,它会被 IDEA / Android Studio 识别为 `desktopApp [hot] 🔥`。这是开发阶段的主要的测试方法。点击,即可启动编译,并运行热重载程序,它可以在你修改源代码并编译后,直接在原有程序窗口同步你的修改。
## commonMain 编写设计
可以看到 commonMain 的 App.kt 已经包含了一些内容了:
```kotlin
@Composable
@Preview
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
val greeting = remember { Greeting().greet() }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(painterResource(Res.drawable.compose_multiplatform), null)
Text("Compose: $greeting")
}
}
}
}
}
```
接下来将对其中的细节进行讲解。
### 基本的 UI 设计思路
#### 结构
KMP 的 UI 是基于一个又一个“盒子”的,在这里,最外层是一个主题 `MaterialTheme`,在主题内部是一个 `Column`,看它的 `modifier` 参数,这里有一个 `fillMaxSize`,表明这个盒子会占据整个窗口。在一个 Column 中,内部的所有组件都会纵向排列。
Column 内部,有一个 `Button`,还有一个被 `AnimatedVisibility` 框起来的 Column,这里我们先不管 AnimatedVisibility 所实现的动画效果,我们看排版结构。前文提到,在 Column 内部,也就是它的 lambda 里面,所有的组件会纵向排列,也就是说 Button 和内层 Column 会在窗口中一个在上一个在下。
#### 图片资源
内层的 Column 又包含了两个组件,一个是图片,一个是文本。这里有一个东西叫图片资源,它们的源文件放在 `commonMain/composeResources/drawable` 中,放在这里的图片会自动被 gradle 注册在一个 object `Res.drawable` 之中,变量名就是它们的文件名。这是 KMP 中最好的调用图片方式。
### 基本的逻辑处理思路
我们来看它的 showContent 变量如何被定义,然后在 Button 被点击时被改变
```kotlin
// 将 showContent 委托给一个 Compose state
var showContent by remember { mutableStateOf(false) }
// 一些排版
// 这里使用一个 lambda 将 showContent 的值反转
Button(onClick = { showContent = !showContent })
```
因为变量 showContent 是函数 AnimatedVisibility 的参数之一,因此,当它的值发生变化时,该函数会被重载,随后触发动画效果,其中的 Column 就会出现 / 消失。
## 更多的排版工具与资源文档
[排版 / 组件](https://m3.material.io/components)
[Google 提供的矢量图标](https://fonts.google.com/icons)
你可以这样使用它们:
```kotlin
Icon(
imageVector = Icons.Default.Favorite, // 在网站上可以直接查到
contentDescription = null, // 图片的文字标记,一般用于无障碍
tint = Color(0xFFE53935) // 注意,alpha 值写在第一位
)
```
## main 函数
刚刚生成的新项目,它的 main 函数一般是这样的:
```kotlin
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "YourProjectName",
) {
App()
}
}
```
在这个 `Window` 函数中,可以用 `state = rememberWindowState(width = 900.dp, height = 800.dp)` 来设定窗口的初始大小。用 `icon = painterResource()` 设定窗口图标。例如:
```kotlin
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = Const.FULL_APP_NAME, // 自己定义一个 Const 对象,很方便的东西
state = rememberWindowState(width = 900.dp, height = 800.dp),
icon = painterResource(Res.drawable.kmp)
) {
App()
}
}
```
## 设置 desktop 编译选项
刚刚生成的新项目,它的 `desktopApp/build.gradle.kts` 一般是这样的:
```kotlin
compose.desktop {
application {
mainClass = "your.package.name.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "your.package.name"
packageVersion = "1.0.0"
}
}
}
```
其实这样就可以编译对应三大桌面平台的安装包了。不过如果你想做的更细致点也是可以的:
```kotlin
compose.desktop {
application {
mainClass = "your.package.name.MainKt"
nativeDistributions {
targetFormats(
// Windows
TargetFormat.Msi,
TargetFormat.Exe,
// macOS
TargetFormat.Dmg,
// Linux
TargetFormat.Deb,
TargetFormat.Rpm,
TargetFormat.AppImage
)
// 你可以在 gradle 文件中用 Kotlin 一样的方法定义变量,这很方便
packageName = appName
packageVersion = appVersion
// 如果可以直接运行,那么就不要写 proguard 这一段
buildTypes.release.proguard {
configurationFiles.from(files("proguard-rules.pro"))
}
windows {
iconFile.set(project.file("icons/icon.ico"))
menuGroup = appName
shortcut = true
dirChooser = true
}
macOS {
iconFile.set(project.file("icons/icon.icns"))
bundleID = packageName
appStore = false
signing {
sign.set(false)
}
}
linux {
iconFile.set(project.file("icons/icon.png"))
menuGroup = appName
shortcut = true
}
}
}
}
```
## 接下来可以看
[目录](../README.md)
[我曾经写的一个简单的小应用](https://github.com/SOR2171/Dices)
[下一篇:复杂逻辑与多平台不同逻辑](./复杂逻辑与多平台不同逻辑.md)
+6 -4
View File
@@ -25,7 +25,7 @@
- build.gradle.kts
- .gitignore
- README.md
- gradlew (二进制脚本,一个bat,一个sh)
- gradlew (二进制脚本,一个 bat,一个 sh)
- gradle.proporties
- local.proporties (默认被 ignore)
- build.gradle.kts
@@ -50,6 +50,8 @@
KMP 的 UI 以 `@Composable` 函数为核心,各个平台的 `main()` 函数入口都会默认调用 `shared/commonMain/kotlin` 中的 `App()` 函数。基本的 UI 也都会在 commonMain 中编写。
在 Composable 函数中,通过各种 UI 函数的相互组合,便可以设计出无限可能的 GUI。
### @Composable 函数
我们先讲 `@Composable`,它是一个注解,用于函数或 lambda 变量:
@@ -61,15 +63,15 @@ fun MyButton(a: Int) {
}
val myCard = @Composable { a: Int ->
TODO() // lambda 不推荐
TODO() // lambda 不推荐大写首字母
} // 变量类型是 @Composable (Int) -> Unit
```
它有几个重要的特点:
- Composable 函数只能在 main 函数或 Composable 函数中被调用。
- Composable 函数只能在 main 函数(描述不严谨)或 Composable 函数中被调用。
- 当 Composable 函数的参数发生变化时,它会被立刻重组(重新触发),受影响的 UI 立刻重新绘制。
- 如果 Composable 函数的 lambda 参数的函数体中出现了 `Compose state` 对象,那么,当它的状态变化时,函数也会被重组。
- 如果 Composable 函数的 lambda 参数的函数体中使用了 `Compose state` 对象的 `value`,那么,当它的状态变化时,lambda 所在函数也会被重组。
### 保护变量值