Files
From_Kotlin_to_Kotlin-Multi…/Learning/第一个KMP应用.md
T

7.2 KiB

第一个 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 已经包含了一些内容了:

@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 就会出现 / 消失。

更多的排版工具与资源文档

KMP 排版设计

KMP 组件

Google 提供的矢量图标

你可以这样使用它们:

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
            }
        }
    }
}

接下来可以看

目录

下一篇:复杂逻辑与多平台不同逻辑

下一篇:全栈 KMP

尝试完成2026年国庆小项目