# 第一个 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 创建的项目,会在 IDE 中添加一个叫 `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 的值反转,这个函数只有 Mutable State 能用 Button(onClick = { showContent.toggle() }) ``` 因为变量 showContent 是函数 AnimatedVisibility 的参数之一,因此,当它的值发生变化时,该函数会被重载,随后触发动画效果,其中的 Column 就会出现 / 消失。 ## 更多的排版工具与资源文档 [KMP 排版设计](https://developer.android.com/develop/ui/compose/layouts/basics?hl=zh-cn) [KMP 组件](https://developer.android.com/develop/ui/compose/components?hl=zh-cn) [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) [下一篇:复杂逻辑与多平台不同逻辑](./复杂逻辑与多平台不同逻辑.md) [下一篇:全栈 KMP](./全栈KMP.md) [尝试完成2026年国庆小项目](https://git.itouc.cn/ITStudio_OUC/KMP_2026_National_Day_Project)