7.2 KiB
第一个 KMP 应用
这里,我推荐刚开始上手 KMP 的朋友们先写仅有 JVM 平台的 KMP 应用。
项目结构
- 项目根目录/
- desktopApp/
- src/
- build.gradle.kts
- shared/
- src/
- commonMain/
- composeResources/
- kotlin/
- commonTest/
- jvmMain/
- commonMain/
- build.gradle.kts
- src/
- build.gradle.kts
- settings.gradle.kts
- desktopApp/
跑起来看看效果
Gradle 默认会注册启动项 hotRun,如果是在 IDEA / Android Studio 创建的项目,会在 IDE 中添加一个叫 desktopApp [hot] 🔥 的配置文件。这是开发阶段的主要的测试方法。点击,即可启动编译,并运行热重载程序,它可以在你修改源代码并编译后,直接在原有程序窗口同步你的修改。
commonMain 编写设计
可以看到 commonMain 的 App.kt 已经包含了一些内容了:
@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 被点击时被改变
// 将 showContent 委托给一个 Compose State
var showContent by remember { mutableStateOf(false) }
// 一些排版
// 这里使用一个 lambda 将 showContent 的值反转,这个函数只有 Mutable State 能用
Button(onClick = { showContent.toggle() })
因为变量 showContent 是函数 AnimatedVisibility 的参数之一,因此,当它的值发生变化时,该函数会被重载,随后触发动画效果,其中的 Column 就会出现 / 消失。
更多的排版工具与资源文档
你可以这样使用它们:
Icon(
imageVector = Icons.Default.Favorite, // 在网站上可以直接查到
contentDescription = null, // 图片的文字标记,一般用于无障碍
tint = Color(0xFFE53935) // 注意,alpha 值写在第一位
)
main 函数
刚刚生成的新项目,它的 main 函数一般是这样的:
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "YourProjectName",
) {
App()
}
}
在这个 Window 函数中,可以用 state = rememberWindowState(width = 900.dp, height = 800.dp) 来设定窗口的初始大小。用 icon = painterResource() 设定窗口图标。例如:
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 一般是这样的:
compose.desktop {
application {
mainClass = "your.package.name.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "your.package.name"
packageVersion = "1.0.0"
}
}
}
其实这样就可以编译对应三大桌面平台的安装包了。不过如果你想做的更细致点也是可以的:
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
}
}
}
}