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 语言指南
相关推荐
花花鱼32 分钟前
android studio 设置让开发更加的方便,比如可以查看变量的类型,参数的名称等等
android·ide·android studio
alexhilton2 小时前
为什么你的App总是忘记所有事情
android·kotlin·android jetpack
AirDroid_cn5 小时前
OPPO手机怎样被其他手机远程控制?两台OPPO手机如何相互远程控制?
android·windows·ios·智能手机·iphone·远程工作·远程控制
尊治5 小时前
手机电工仿真软件更新了
android
杂雾无尘7 小时前
开发者必看,全面解析应用更新策略,让用户无法拒绝你的应用更新!
ios·xcode·swift
xiangzhihong88 小时前
使用Universal Links与Android App Links实现网页无缝跳转至应用
android·ios
车载应用猿9 小时前
基于Android14的CarService 启动流程分析
android
没有了遇见9 小时前
Android 渐变色实现总结
android
Digitally10 小时前
如何将iPhone备份到Mac/MacBook
macos·ios·iphone
帅次11 小时前
【iOS设计模式】深入理解MVC架构 - 重构你的第一个App
ios·swiftui·objective-c·iphone·swift·safari·cocoapods