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 Multiplatform

我们用通用的 Kotlin 编写可复用的代码部分,借助多平台支持,这些代码可以在所有目标平台上运行。例如,调用 REST API 获取数据就是一个很适合放在公共部分的逻辑。

3.2. 平台间源码复用

Kotlin 多平台通过源码集(source sets)组织代码,使得依赖关系清晰,并实现代码复用:

Kotlin Multiplatform Shared All

默认情况下,所有平台特定的源码集都依赖于公共源码集。公共代码可以使用 Kotlin 提供的库来完成常见任务,如 HTTP 请求、数据序列化、并发控制等。

同时,平台特定的版本也提供了可以利用平台特性的库。因此,我们可以将业务逻辑放在公共部分,而 UI 等部分则使用原生能力实现。

Kotlin 还支持选择性共享代码:

Kotlin Multiplatform Shared Hierarchy

如上图所示,公共代码可以共享给所有平台,也可以只共享给某些平台(如 Linux、Windows、macOS)。

3.3. 开发平台特定的 API

有时候我们希望在公共代码中定义和使用平台特定的 API。例如,某些任务在特定平台上有更高效或更合适的实现方式。

Kotlin 提供了 expectedactual 声明机制来实现这一点:

Kotlin Multiplatform Platform Specific APIs

公共源码集中声明一个函数为 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 中,我们可以使用项目模板快速创建多平台项目:

Kotlin Multiplatform IDEA Wizard

项目创建后,结构如下:

Kotlin Multiplatform Project Structure

默认包含 common、JVM、JS、Native 的源码目录和配置。我们可以手动增删目标平台。

4.2. 编写公共代码

公共代码放在 commonMaincommonTest 中。我们先写一个简单的计算器逻辑:

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 会提示选择目标平台:

Kotlin Multiplatform Running Unit Tests

我们可以选择多个平台同时运行测试。

5. 针对 JVM 平台:Kotlin/JVM

Kotlin 最初就是为 JVM 设计的。它解决了 Java 的一些痛点,比如冗长的语法和并发处理的复杂性。Kotlin 提供了结构化并发(协程)等特性,非常适合服务端开发。

5.1. Kotlin/JVM 编译器

Kotlin 编译器会将 Kotlin 源码编译为 JVM 字节码。我们可以使用 kotlinckotlinc-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 上运行:

Kotlin Multiplatform Native CLI-1

8. 总结

在本教程中,我们学习了 Kotlin 多平台编程的基本概念和实现方式。我们创建了一个多平台项目,并在 JVM、JS、Native 上复用了公共逻辑。

我们还使用 Kotlin/JS 构建了一个 React 前端界面,以及使用 Kotlin/Native 构建了一个命令行工具。

Kotlin 多平台让我们真正实现了“写一次,跑多处”,大大提升了开发效率。

完整代码请参考:GitHub


原始标题:Introduction to Multiplatform Programming in Kotlin