首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

  • 24-12-06 00:05
  • 2889
  • 9274
juejin.cn

前言

在 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 语言与其它语言的互操作性。

                                                                                                                                                                                             
平台(部署)语言互操作性
AndroidKotlin ↔️ JavaKotlin 在设计时就考虑了 Java 互操作性。可以从 Kotlin 中自然地调用现存的 Java 代码,并且在 Java 代码中也可以很顺利地调用 Kotlin 代码。
iOSKotlin ↔️ 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/WasmKotlin ↔️ JS
Kotlin ↔️ C/C++
Web based on Kotlin/JSKotlin ↔️ JSKotlin/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。

K2-Kotlin-Compiler-2024-11-20-094534.png

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,以此达到原生性能的要求在各平台运行。

参考文档

  1. Expected and actual declarations

  2. kotlin 语言指南

注:本文转载自juejin.cn的划水健儿的文章"https://juejin.cn/post/7441222836545601570"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top