1. 引言
在本教程中,我们将了解 Kotlin 的多平台编程。我们将开发一个简单的应用程序,目标平台包括 JVM、JS 和 Native。
这也将帮助我们理解多平台编程的优势,以及在哪些场景下可以有效应用它。
2. 什么是多平台编程?
在开发中,我们常常会编写一些不依赖具体运行平台的代码。例如调用一个 REST API 获取数据,并进行处理后再返回结果。这类逻辑在不同平台上往往需要重复实现:Java 用于后端,JS 用于前端,Android 或 iOS 用于移动端。
如果能写一次代码,就能在多个平台上运行,岂不是很棒?这就是多平台编程的核心理念。而 Kotlin 多平台(Kotlin Multiplatform)正好能满足这一需求。我们将在本教程中看到它是如何实现的。
Java 编译成字节码后可以在任何 JVM 上运行,这是“一次编写,到处运行”的基础。但即使如此,仍需要目标平台上的 JVM 来执行。
而 Kotlin 多平台更进一步,它让相同的代码可以直接运行在 JVM、JS、甚至原生平台上,不依赖虚拟机。这成为 Kotlin 的一大亮点。
此外,它还能显著减少为不同平台编写和维护相同逻辑的工作量。
3. Kotlin 如何支持多平台编程?
在开始实践之前,我们先来了解 Kotlin 是如何支持多平台编程的。本节将介绍 Kotlin 提供的一些工具和技术。
3.1. 基础知识
Java 代码之所以能在不同 JVM 上运行,是因为 Java 编译器将其转换为字节码。Kotlin、Groovy、Scala 等语言也利用了这一点,它们各自有编译器生成兼容的字节码。
同理,Kotlin 通过平台特定的编译器和库(如 Kotlin/JVM、Kotlin/JS、Kotlin/Native)来实现跨平台能力:
我们用通用的 Kotlin 编写可复用的代码部分,借助多平台支持,这些代码可以在所有目标平台上运行。例如,调用 REST API 获取数据就是一个很适合放在公共部分的逻辑。
3.2. 平台间源码复用
Kotlin 多平台通过源码集(source sets)组织代码,使得依赖关系清晰,并实现代码复用:
默认情况下,所有平台特定的源码集都依赖于公共源码集。公共代码可以使用 Kotlin 提供的库来完成常见任务,如 HTTP 请求、数据序列化、并发控制等。
同时,平台特定的版本也提供了可以利用平台特性的库。因此,我们可以将业务逻辑放在公共部分,而 UI 等部分则使用原生能力实现。
Kotlin 还支持选择性共享代码:
如上图所示,公共代码可以共享给所有平台,也可以只共享给某些平台(如 Linux、Windows、macOS)。
3.3. 开发平台特定的 API
有时候我们希望在公共代码中定义和使用平台特定的 API。例如,某些任务在特定平台上有更高效或更合适的实现方式。
Kotlin 提供了 expected 和 actual 声明机制来实现这一点:
公共源码集中声明一个函数为 expected,平台特定的源码集必须提供一个 actual 实现。公共代码不关心具体实现,只要目标平台提供了即可。
这些声明可用于函数、类、接口、枚举、属性和注解。
3.4. 工具支持
Kotlin 是 JetBrains 的产物,而 IntelliJ IDEA 是其知名的 IDE。因此,多平台项目在 IntelliJ IDEA 中有良好的支持。
IntelliJ 提供了创建多平台项目的模板,简化了项目创建流程。模板会自动应用 kotlin-multiplatform
插件:
plugins {
kotlin("multiplatform") version "1.4.0"
}
该插件配置项目以支持多平台编译。典型的 build.gradle.kts
配置如下:
kotlin {
jvm {
withJava()
}
js {
browser {
binaries.executable()
}
}
sourceSets {
val commonMain by getting {
dependencies {
...
}
}
val commonTest by getting {
dependencies {
...
}
}
val jvmMain by getting {
dependencies {
...
}
}
val jsMain by getting {
dependencies {
...
}
}
}
}
每个目标平台可以有多个编译任务(如 main 和 test)。插件会自动处理这些任务,生成对应平台的构建产物。
4. 实战:多平台计算器应用
接下来我们将动手实践,开发一个简单的计算器应用,其中包含可在多个平台(JVM、JS、Native)上复用的代码。
4.1. 创建多平台项目
在 IntelliJ IDEA 中,我们可以使用项目模板快速创建多平台项目:
项目创建后,结构如下:
默认包含 common、JVM、JS、Native 的源码目录和配置。我们可以手动增删目标平台。
4.2. 编写公共代码
公共代码放在 commonMain
和 commonTest
中。我们先写一个简单的计算器逻辑:
fun add(num1: Double, num2: Double): Double {
val sum = num1 + num2
writeLogMessage("The sum of $num1 & $num2 is $sum", LogLevel.DEBUG)
return sum
}
fun subtract(num1: Double, num2: Double): Double {
val diff = num1 - num2
writeLogMessage("The difference of $num1 & $num2 is $diff", LogLevel.DEBUG)
return diff
}
fun multiply(num1: Double, num2: Double): Double {
val product = num1 * num2
writeLogMessage("The product of $num1 & $num2 is $product", LogLevel.DEBUG)
return product
}
fun divide(num1: Double, num2: Double): Double {
val division = num1 / num2
writeLogMessage("The division of $num1 & $num2 is $division", LogLevel.DEBUG)
return division
}
这里我们调用了一个未定义的函数 writeLogMessage
,它需要平台特定实现:
enum class LogLevel {
DEBUG, WARN, ERROR
}
internal expect fun writeLogMessage(message: String, logLevel: LogLevel)
4.3. 编写公共代码的测试
我们为这些函数编写单元测试:
@Test
fun testAdd() {
assertEquals(4.0, add(2.0, 2.0))
}
@Test
fun testSubtract() {
assertEquals(0.0, subtract(2.0, 2.0))
}
@Test
fun testMultiply() {
assertEquals(4.0, multiply(2.0, 2.0))
}
@Test
fun testDivide() {
assertEquals(1.0, divide(2.0, 2.0))
}
测试运行时,IDEA 会提示选择目标平台:
我们可以选择多个平台同时运行测试。
5. 针对 JVM 平台:Kotlin/JVM
Kotlin 最初就是为 JVM 设计的。它解决了 Java 的一些痛点,比如冗长的语法和并发处理的复杂性。Kotlin 提供了结构化并发(协程)等特性,非常适合服务端开发。
5.1. Kotlin/JVM 编译器
Kotlin 编译器会将 Kotlin 源码编译为 JVM 字节码。我们可以使用 kotlinc
或 kotlinc-jvm
命令行工具进行编译。
如果项目中包含 Java 代码,可以在 Gradle 中启用 Java 支持:
jvm {
withJava()
}
还可以指定目标 JVM 版本:
jvm {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
}
5.2. 编写 JVM 平台代码
我们在 jvmMain
中实现 writeLogMessage
:
internal actual fun writeLogMessage(message: String, logLevel: LogLevel) {
println("Running in JVM: [$logLevel]: $message")
}
我们还可以在 JVM 模块中编写 Java 代码与 Kotlin 协同工作:
public static Double square(Double number) {
return CalculatorKt.multiply(number, number);
}
注意:Kotlin 函数在 Java 中会作为静态方法调用,由编译器自动生成类。
6. 针对 JavaScript 平台:Kotlin/JS
JavaScript 是前端开发的主流语言,Kotlin 也支持将其编译为 JS 代码。
6.1. Kotlin/JS 编译器
Kotlin/JS 编译器将 Kotlin 转换为 JavaScript 代码。它默认生成符合 ES5 标准的代码。
Kotlin 1.4 引入了基于 IR(中间表示)的新编译器后端,支持更高级的优化,例如生成更小的 JS 包和 TypeScript 类型定义。
启用 IR 编译器只需修改 Gradle 配置:
kotlin {
js(IR) {
}
}
可以选择目标环境(浏览器或 Node.js):
kotlin {
js {
browser {
}
}
}
6.2. 编写 JS 平台代码
我们用 React 实现一个简单的前端界面。首先实现 writeLogMessage
:
internal actual fun writeLogMessage(message: String, logLevel: LogLevel) {
when (logLevel) {
LogLevel.DEBUG -> console.log("Running in JS: $message")
LogLevel.WARN -> console.warn("Running in JS: $message")
LogLevel.ERROR -> console.error("Running in JS: $message")
}
}
添加 React 依赖:
val jsMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.7.2")
implementation("org.jetbrains:kotlin-react:16.13.1-pre.110-kotlin-1.4.10")
implementation("org.jetbrains:kotlin-react-dom:16.13.1-pre.110-kotlin-1.4.10")
implementation("org.jetbrains:kotlin-styled:1.0.0-pre.110-kotlin-1.4.10")
}
}
HTML 页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Client</title>
</head>
<body>
<script src="kotlin-multiplatform.js"></script>
<div id="root"></div>
</body>
</html>
主函数启动 React:
fun main() {
window.onload = {
render(document.getElementById("root")) {
child(Calculator::class) {
attrs {
value = "0"
}
}
}
}
}
React 组件:
@JsExport
class Calculator(props: CalculatorProps) : RComponent<CalculatorProps, CalculatorState>(props) {
init {
state = CalculatorState(props.value)
}
override fun RBuilder.render() {
styledLabel {
css {
}
+ "Enter a Number: "
}
styledInput {
css {
}
attrs {
type = InputType.number
value = state.value
onChangeFunction = { event ->
setState(
CalculatorState(value = (event.target as HTMLInputElement).value)
)
}
}
}
styledDiv {
css {
}
+"Square of the Input: ${
multiply(state.value.toDouble(), state.value.toDouble())}"
}
}
}
运行命令:
gradlew jsRun
启用 CSS 支持:
browser {
commonWebpackConfig {
cssSupport.enabled = true
}
}
7. 针对原生平台:Kotlin/Native
Kotlin/Native 将 Kotlin 编译为原生二进制文件,适用于桌面和移动端开发。
7.1. Kotlin/Native 编译器
Kotlin/Native 使用 LLVM 编译器后端,支持多种平台:
- Linux (x86_64, arm32, arm64)
- Windows (x86_64, x86)
- Android (arm32, arm64, x86)
- iOS (arm32, arm64, x86_64)
- macOS (x86_64)
- WebAssembly (wasm32)
Gradle 中根据操作系统选择目标平台:
kotlin {
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
}
7.2. 编写 Native 平台代码
我们在 nativeMain
中实现 writeLogMessage
:
internal actual fun writeLogMessage(message: String, logLevel: LogLevel) {
println("Running in Native: [$logLevel]: $message")
}
配置入口函数:
nativeTarget.apply {
binaries {
executable {
entryPoint = "com.baeldung.kotlin.multiplatform.main"
}
}
}
主函数:
fun main() {
println("Enter a Number:")
val number = readLine()!!.toInt()
println("Square of the Input: ${multiply(number, number)}")
}
在 Windows 上运行:
8. 总结
在本教程中,我们学习了 Kotlin 多平台编程的基本概念和实现方式。我们创建了一个多平台项目,并在 JVM、JS、Native 上复用了公共逻辑。
我们还使用 Kotlin/JS 构建了一个 React 前端界面,以及使用 Kotlin/Native 构建了一个命令行工具。
Kotlin 多平台让我们真正实现了“写一次,跑多处”,大大提升了开发效率。
完整代码请参考:GitHub ✅