前言
在 Kotlin Multiplatform 项目中做逻辑共享,如果逻辑不依赖平台特性,那么就把所有逻辑都写在 commonMain 中,所有平台(androidMain,iosMain,jsMain,jvmMain等)直接共享;如果逻辑依赖平台特性,那么就需要通过 expect 和 actual 机制来访问平台特性,然后把部分逻辑写在各自平台(androidMain,iOSMain,jsMain,jvmMain等)中,以此实现平台共享。
当然,也可以将依赖平台特性相关逻辑封装成通用库(比如:kotlinx-datetime),让所有逻辑都在commonMain 中(看似)。
expect 和 actual 声明
定义
expect 和 actual 声明定义了一套规则:
-
expect:表示预期声明,只能用在commonMain代码中,用于描述跨平台代码的功能,不提供具体实现。 -
actual:表示实际声明,用在androidMain,iosMain,jsMain,jvmMain等平台代码中,与expect声明一一对应,提供具体实现。
这感觉 expect 像是声明接口,而 actual 是像是声明接口实现,在 commonMain 中不关心平台具体实现,到了每个平台才实现并处理相关逻辑。
文件名命名规则
首先,expect 声明 和 actual声明所相关的.kt文件需要有相同的命名空间。其次,特定平台需要关联其平台后缀。
如果在 commonMain 使用 expect声明所相关的.kt文件为:commonMain/kotlin/Platform.kt,那么特定平台使用 actual 声明所相关的.kt 文件为:
-
Android 平台:
androidMain/kotlin/Platform.android.kt -
iOS 平台:
iosMain/kotlin/Platform.ios.kt -
Desktop 平台:
jvmMain/kotlin/Platform.jvm.kt -
Web 平台(js):
jsMain/kotlin/Platform.js.kt -
Web 平台(wasm):
wasmJsMain/kotlin/Platform.wasmJs.kt
当编译代码生成各个平台产物时,Kotlin 编译器会合并彼此对应的expect声明和actual声明,让 commonMain 中使用 expect 声明的地方都使用各个平台的 actual 声明。
使用
在 commonMain中使用expect可以声明函数、属性、类、接口、枚举或注解:
kotlin 代码解读复制代码//在 commonMain 中
//函数:获取当前时间字符串
expect fun getCurrentTime(pattern: String): String
//属性:获取平台名称
expect val platformName: String
//类:打印日志
expect class Logger {
fun log(tag: String, message: String)
}
//接口:获取设备信息
expect interface DeviceInfo {
val osName: String
val deviceModel: String
}
//枚举:屏幕方向枚举
expect enum class ScreenOrientation {
PORTRAIT, LANDSCAPE
}
//注解
@OptIn(ExperimentalMultiplatform::class)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@OptionalExpectation
expect annotation class Serializable()
在 expect 声明类、接口、枚举和注解时,会遇到 warning:
js 代码解读复制代码'expect'/'actual' classes (including interfaces, objects, annotations, enums, and 'actual' typealiases) are in Beta. You can use -Xexpect-actual-classes flag to suppress this warning. Also see: https://youtrack.jetbrains.com/issue/KT-61573
可以在 build.gradle.kt 添加配置:
kotlin 代码解读复制代码kotlin {
targets.configureEach {
compilations.configureEach {
compileTaskProvider.get().compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}
}
在androidMain,iOSMain,jsMain,jvmMain等平台中使用 actual 声明为:
kotlin 代码解读复制代码//函数:获取当前时间字符串
actual fun getCurrentTime(pattern: String): String {
return "TODO"
}
//属性:获取平台名称
actual val platformName: String = "TODO"
//类:打印日志
actual class Logger {
actual fun log(tag: String, message: String) {
//TODO
}
}
//接口:获取设备信息
actual interface DeviceInfo {
actual val osName: String
actual val deviceModel: String
}
//枚举:屏幕方向枚举
actual enum class ScreenOrientation {
PORTRAIT, LANDSCAPE
}
//注解
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
actual annotation class Serializable()
假设现在需要在 UI 上显示当前时间,时间格式为 yyyy-MM-dd HH:mm:ss。那么,在
commonMain/kotlin/Platform.kt 中:
kotlin 代码解读复制代码//函数:获取当前时间字符串
expect fun getCurrentTime(pattern: String): String
在 Android 平台中,androidMain/kotlin/Platform.android.kt:
kotlin 代码解读复制代码import android.text.format.DateFormat
import java.util.Date
actual fun getCurrentTime(pattern: String): String {
return DateFormat.format(pattern, Date()).toString()
}
在 iOS 平台中,iosMain/kotlin/Platform.ios.kt:
kotlin 代码解读复制代码import platform.Foundation.NSDate
import platform.Foundation.NSDateFormatter
actual fun getCurrentTime(pattern: String): String {
val dateFormatter = NSDateFormatter()
dateFormatter.setDateFormat(pattern)
return dateFormatter.stringFromDate(NSDate())
}
在 Desktop 平台中,jvmMain/kotlin/Platform.jvm.kt:
kotlin 代码解读复制代码import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
actual fun getCurrentTime(pattern: String): String {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(pattern))
}
在 Web 平台中(js):jsMain/kotlin/Platform.js.kt
kotlin 代码解读复制代码actual fun getCurrentTime(pattern: String): String {
return kotlin.js.Date().toLocaleString()
}
构建产物
这里以上面的代码,以及 Android 平台为例子。
假设在 Kotlin Multiplatform 项目中的逻辑模块为::shared,那么在命令行执行./gradlew :shared:assembleDebug后,shared 目录下会生成:/build/outputs/aar/shared-debug.aar产物,使用 AndroidStuido 打开 shared-debug.aar,里面 classes.jar 下相关 class 文件为:
shell代码解读复制代码classes.jar -- ScreenOrientation.class -- Platform_androidKt.class -- Logger.class -- Serializable.class -- DeviceInfo.class
枚举 ScreenOrientation.class:
kotlin 代码解读复制代码public final enum class ScreenOrientation private constructor() : kotlin.Enum {
PORTRAIT,
LANDSCAPE;
}
属性和方法 Platform_androidKt.class:
kotlin 代码解读复制代码public val platformName: kotlin.String /* compiled code */
public fun getCurrentTime(pattern: kotlin.String): kotlin.String { /* compiled code */ }
类 Logger.class:
kotlin 代码解读复制代码public final class Logger public constructor() {
public final fun log(tag: kotlin.String, message: kotlin.String): kotlin.Unit { /* compiled code */ }
}
注解 Serializable.class:
kotlin 代码解读复制代码@kotlin.annotation.Target @kotlin.annotation.Retention public final annotation class Serializable public constructor() : kotlin.Annotation {
}
接口 DeviceInfo.class:
kotlin 代码解读复制代码public interface DeviceInfo {
public abstract val osName: kotlin.String
public abstract val deviceModel: kotlin.String
}
编译得到的 Android 平台产物,会把在 commonMain/kotlin/Platform.kt 中使用 expect 声明的地方都使用androidMain/kotlin/Platform.android.kt中的 actual 声明。
另外,构建每个平台产物命令为:
| 平台(部署) | 构建命令 | 产物类型 | 产物位置 |
|---|---|---|---|
| Android | ./gradlew :shared:assembleRelease | .aar | shared/build/outputs/aar/shared-release.aar |
| iOS | ./gradlew :shared:linkReleaseFrameworkIosArm64 ./gradlew :shared:linkReleaseFrameworkIosX64 ./gradlew :shared:linkReleaseFrameworkIosSimulatorArm64 |
.framework |
shared/build/bin/iosArm64/releaseFramework/shared.framework shared/build/bin/iosX64/releaseFramework/shared.framework shared/build/bin/iosSimulatorArm64/releaseFramework/shared.framework |
| Desktop | ./gradlew :shared:jvmJar | .jar | shared/build/libs/shared-jvm.jar |
| JavaScript | ./gradlew :shared:jsProductionExecutableCompileSync | .js | shared/build/compileSync/js/main/productionExecutable/kotlin/kmp-shared.js |
| WebAssembly | ./gradlew :shared:wasmJsProductionExecutableCompileSync | .wasm | build/compileSync/wasmJs/main/productionExecutable/kotlin/KotlinProject-shared-wasm-js.wasm |
构建所有平台产物命令为./gradlew :shared:assemble。
互操作性
通过 expect 声明和 actual 声明机制可以访问不同平台的原生 API。但这种机制的具体实现依赖于Kotlin 语言与其它语言的互操作性。
| 平台(部署) | 语言 | 互操作性 |
|---|---|---|
| Android | Kotlin ↔️ Java | Kotlin 在设计时就考虑了 Java 互操作性。可以从 Kotlin 中自然地调用现存的 Java 代码,并且在 Java 代码中也可以很顺利地调用 Kotlin 代码。 |
| iOS | Kotlin ↔️ C/C++ Kotlin ↔️ Swift/Objective-C | POSIX、 gzip、 OpenGL、 Metal、 Foundation 以及许多其他流行库与 Apple 框架都已预先导入并作为 Kotlin/Native 库包含在编译器包中。 |
| Desktop(JVM) | Kotlin ↔️ Java | |
| Server-side(JVM) | Kotlin ↔️ Java | |
| Web based on Kotlin/Wasm | Kotlin ↔️ JS Kotlin ↔️ C/C++ | |
| Web based on Kotlin/JS | Kotlin ↔️ JS | Kotlin/JS 提供了转换 Kotlin 代码、Kotlin 标准库的能力,并且兼容 JavaScript 的任何依赖项。Kotlin/JS 的当前实现以 ES5为目标。 |
Kotlin 语言与 Java 语言具有无缝互操作性,与 C/C++ 和 Swift/Objective-C 语言具有一定程度上的互操作性,与 JavaScript 语言具有很大程度上的互操作性。
编译目标
在commonMain和androidMain 或 iosMain 或 jvmMain 或 jsMain 或 wasmJsMain 平台的代码,最终都会通过编译器转换为平台可执行的文件。
Kotlin Multiplatform 的跨平台能力,是通过编译器编译目标来实现的:Kotlin/JVM, Kotlin/Native,Kotlin/JS。
![]()
Kotlin Source Code:commonMain 和 androidMain 或 iosMain 或 jvmMain 或 jsMain 或 wasmJsMain中的代码.
Frontend Compiler:负责解析 Kotlin 代码、验证语法和语义等,并生成跨平台的中间表示(IR)。
Intermediate Representation(IR):将 Kotlin 代码转换为与平台无关的中间表示(IR)。
Backend Compiler:IR 被转换为特定平台的产物。
-
JVM IR Backend:将 IR 转换为 JVM 字节码(
.class文件) -
Native Backend:将 IR 转换为平台原生的机器码(
so,.dylib文件等) -
JS IR Backend:将 IR 转换为 JavaScript 文件(
.js文件) -
Wasm Backend:将 IR 转换为 WebAssembly 文件(
.wasm文件)
依赖倒置原则
expect 声明和 actual 声明机制与依赖倒置原则是完美结合的。
依赖倒置原则的核心是:
-
高层模块不应该依赖底层模块,两者都应该依赖于抽象。
-
抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
高层模块不依赖于底层模块:
- 高层模块只依赖于
expect,与底层模块具体的actual无关。
抽象不依赖具体实现:
-
高层模块的
expect声明,定义抽象,依赖抽象。 -
底层模块的
actual声明,实现抽象,依赖抽象。
依赖倒置原则带来的好处是:
-
解耦:各个平台各自独立实现,跨平台逻辑完全隔离。
-
灵活:不同实现或新增实现,都无需修改高层代码。
在 Kotlin Multiplatform 项目中开发,遇到跨平台问题时,可以优先考虑使用依赖倒置原则来解决。比如:实现网络状态监听器。
总结
在 Kotlin Multiplatform 项目中,想要访问平台不同特性,可以使用expect 和 actual声明,expect 和 actual 声明遵循依赖倒置原则,expect声明定义与平台无关的抽象,actual声明实现与平台相关的抽象。
使用 expect 声明在 commonMain 中定义跨平台能力,使用 actual 声明在androidMain 或 iosMain 或 jvmMain 或 jsMain 或 wasmJsMain中实现跨平台能力。在构建时,Kotlin 编译器会合并彼此对应的expect声明和actual声明。
在不同平台上,Kotlin 语言与 Java 或 C/C++ 或 Swift/Object-C 或 JS 语言具有一定程度的互操作性。
编译器将 Kotlin 源码,通过 frontend compiler,IR,backend compiler,最终转换为各平台可执行文件.class,.so, .js,.wasm,以此达到原生性能的要求在各平台运行。
评论记录:
回复评论: