KMP 中的 expect 和 actual 声明

引言

在 KMP 中做逻辑共享,如果逻辑不依赖平台特性,那么就把所有逻辑都写在 commonMain 中,所有平台(androidMainiosMainjsMainjvmMain等)直接共享;如果逻辑依赖平台特性,那么就需要通过 expectactual 机制来访问平台特性,然后把部分逻辑写在各自平台(androidMainiOSMainjsMainjvmMain等)中,以此实现平台共享。

当然,也可以将依赖平台特性相关逻辑封装成通用库(比如:kotlinx-datetime),让所有逻辑都在commonMain 中(看似)。

expect 和 actual 声明

定义

expectactual 声明定义了一套规则:

  • expect:表示预期声明,只能用在 commonMain代码中,用于描述跨平台代码的功能,不提供具体实现。
  • actual:表示实际声明,用在androidMainiosMainjsMainjvmMain等平台代码中,与 expect 声明一一对应,提供具体实现。

这感觉 expect 像是声明接口,而 actual 是像是声明接口实现,在 commonMain 中不关心平台具体实现,到了每个平台才实现并处理相关逻辑。

文件名命名规则

首先,actualexpect声明相关.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):wasmMain/kotlin/Platform.wasm.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")
        }
    }
}

}

那么,在androidMainiOSMainjsMainjvmMain等平台中使用 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 平台为例子。

假设在 kmp 项目中的逻辑模块为::shared,那么在命令行执行./gradlew :shared:assembleDebug后,shared 目录下会生成:/build/outputs/aar/shared-debug.aar产物,使用 AndroidStuido 打开 shared-debug.aar,里面 classes.jar 下相关 class 文件为:

php 复制代码
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<ScreenOrientation> {
    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 声明。

互操作性

通过 expectactual 声明机制可以访问不同平台的原生 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 语言具有很大程度上的互操作性。

编译目标

commonMainandroidMainiosMainjvmMainjsMainwasmMain 平台的代码,最终都会通过编译器转换为平台可执行的文件。

KMP 的跨平台能力,是通过编译器编译目标来实现的:Kotlin/JVMKotlin/NativeKotlin/JS

Kotlin Source CodecommonMainandroidMainiosMainjvmMainjsMainwasmMain中的代码.

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文件)

依赖倒置原则

expectactual 声明机制与依赖倒置原则是完美结合的。

依赖倒置原则的核心是:

  • 高层模块不应该依赖底层模块,两者都应该依赖于抽象。
  • 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

高层模块不依赖于底层模块:

  • 高层模块只依赖于expect,与底层模块具体的 actual 无关。

抽象不依赖具体实现:

  • 高层模块的 expect 声明,定义抽象,依赖抽象。
  • 底层模块的 actual 声明,实现抽象,依赖抽象。

依赖倒置原则带来的好处是:

  • 解耦:各个平台各自独立实现,跨平台逻辑完全隔离。
  • 灵活:不同实现或新增实现,都无需修改高层代码。

在 KMP 项目中开发,遇到跨平台问题时,可以优先考虑使用依赖倒置原则来解决。比如:网络状态监听器。

总结

在 KMP 项目中,想要访问平台不同特性,可以使用expectactual声明,expectactual 声明遵循依赖倒置原则,expect声明定义与平台无关的抽象,actual声明实现与平台相关的抽象。

使用 expect 声明在 commonMain 中定义跨平台能力,使用 actual 声明在androidMainiosMainjvmMainjsMainwasmMain中实现跨平台能力。在构建时,Kotlin 编译器会合并彼此对应的expect声明和actual声明。

在不同平台上,Kotlin 语言与 Java 或 C/C++ 或 Swift/Object-C 或 JS 语言具有一定程度的互操作性。

编译器将 Kotlin 源码,通过 frontend compiler,IR,backend compiler,最终转换为各平台可执行文件.class.so.js.wams,以此达到原生性能的要求在各平台运行。

参考文档

  1. Expected and actual declarations
  2. kotlin 语言指南
相关推荐
建群新人小猿1 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神2 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛2 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法3 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
lzhdim3 小时前
iPhone 17 Air看点汇总:薄至6mm 刷新苹果轻薄纪录
ios·iphone
安和昂3 小时前
【iOS】知乎日报第四周总结
ios
NotesChapter4 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快5 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl6 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5