支持多平台编程是 Kotlin 的主要优势之一。它减少了为不同平台编写和维护相同代码所花费的时间,同时保留了本机编程的灵活性和优势。
1. 基本概念
-
KMM:Kotlin Multiplatform for mobile(移动设备的 Kotlin 多平台)
-
KMM 多平台的主要用例之一是在移动平台之间共享应用程序逻辑代码
-
如果要实现本机 UI 或使用平台 API 时,需要编写特定于平台的代码
-
KMM 当前处于 beta 阶段,已经几乎稳定
-
Kotlin roadmap
-
以下为 Kotlin 团队的优先事项
-
K2 compiler:对 Kotlin 编译器的重写,针对速度、并行性和统一性进行了优化。它还将让我们介绍许多预期的语言功能。在 1.9.0 中,K2 编译器已经达到了 JVM 的 Beta 阶段。此版本还增加了对 Kotlin/Native 的支持,并改进了对 Kotlin/JS 的支持,下一步是使 K2 编译器稳定并发布 Kotlin 2.0
-
K2-based IntelliJ plugin:基于 K2 的 IntelliJ 插件:更快的代码完成、突出显示和搜索,以及更稳定的代码分析。
-
Kotlin Multiplatform:通过改进工具链稳定性和文档,并确保兼容性保证,将技术推广到 Stable
- Kotlin 多平台移动版于 2022 年 10 月进入测试版。到 2023 年底,我们希望将其提升为稳定版,这意味着即使在保守的情况下也可以安全使用。
- 与 Google 合作开发了新的内存分配器,该分配器应该可以提高运行时性能和内存消耗。开发已经完成,分配器在 Kotlin 1.9.0 中可通过选择加入获得,并将在 Kotlin 1.9.20 (KT-55364) 中默认启用(致力于通过并行进行更多垃圾收集工作来减少垃圾收集暂停)。旧内存管理器在 Kotlin 1.8.20 中已弃用,并将在 Kotlin 1.9.20 中删除
-
-
-
-
Compose Multiplatform 是 JetBrains 基于 Kotlin 和 Jetpack Compose 的声明式 UI 框架
-
iOS 处于 Alpha 状态
-
2. 环境准备
-
安装必要的工具
-
JDK(推荐 17 LTS)
-
Kotlin Multiplatform Mobile 插件
- Android Studio Settings/Preferences | Plugins
- 搜索 Kotlin Multiplatform Mobile,安装重启
-
Kotlin 插件
- 通常和 Android Studio 捆绑,建议升级到 1.9.0 或以上
- 如果使用了 Jetpack Compose,注意 compose compiler 和 Kotlin 版本的兼容,例如:
- Kotlin 1.9.10 -> compose compiler 1.5.3
- Kotlin 1.9.0 -> compose compiler 1.5.0
-
检查环境的工具 - KDoctor
-
KDoctor 仅适用于 macOS
-
安装:
brew install kdoctor
-
执行:
kdoctor
-
根据输出结果,安装缺失的工具
- 通常需要安装:Cocoapods,Ruby
-
3. 创建工程
-
KMM 工程使用 gradle 来构建
-
工程整体结构,这里使用 kmm 重写 UpStorage 库为例:
-
├── androidApp
-
├── build.gradle.kts
-
├── gradle
-
├── gradle.properties
-
├── iosApp
-
├── kmmStorageShared
-
└── settings.gradle.kts
-
整个工程分为全局配置部分(build.gradle.kts,gradle,gradle.properties,settings.gradle.kts)和 Modules 部分(kmmStorageShared,androidApp,iosApp)
-
settings.gradle.kts:主要用来引入其他模块
rubyrootProject.name = "UpStorage" include(":androidApp") include(":kmmStorageShared") ```
-
build.gradle.kts:主要用来引入 Maven 源,和 gradle 插件
scssbuildscript { repositories { maven(url = "https://mdpm.haier.net/nexus/repository/public") 。。。 } dependencies { classpath(libs.bundles.plugins) // 包含的插件有: // AGP: com.android.tools.build:gradle:8.1.1 // Kotlin: org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10 } } allprojects { repositories { maven(url = "https://mdpm.haier.net/nexus/repository/public") 。。。 } } ```
-
gradle.properties
markdown# 共享本机代码中使用自定义 cinterop 库 kotlin.mpp.enableCInteropCommonization=true # 新的源码布局 kotlin.mpp.androidSourceSetLayoutVersion=2 ############################################################################################### * common * | * +-----------------+-------------------+ * | | * * native ... * * | * | * | * +----------------------+--------------------+-----------------------+ * | | | | * * apple linux mingw androidNative * * | * +-----------+------------+------------+ * | | | | * * macos ios tvos watchos ```
-
gradle
- 建议使用 8.0 或以上版本
inidistributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https://services.gradle.org/distributions/gradle-8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ```
-
-
Modules 部分介绍
-
kmmStorageShared:是一个纯的 Kotlin 模块,中包含 Android 和 iOS 应用程序通用的逻辑
-
模块结构如下:
-
├── build.gradle.kts
-
├── kmmStorageShared.podspec
-
└── src
-
├── androidMain
-
├── commonMain
-
└── iosMain
-
build.gradle.kts:
- 引入插件
javascriptplugins { kotlin("multiplatform") // kotlin 多平台插件 kotlin("native.cocoapods") // 如果 IOS 使用 cocoapods,引入此插件 id("com.android.library") // 安卓库 } ``` - 其它配置 - ``` kotlin { targetHierarchy.default() // 新的默认的源码布局 // 支持的 IOS 平台 iosX64() iosArm64() iosSimulatorArm64() cocoapods { summary = "Some description for the Shared Module" homepage = "Link to the Shared Module homepage" version = "1.0" license = "MIT" ios.deploymentTarget = "11.0" podfile = project.file("../iosApp/Podfile" framework { baseName = "kmmStorageShared" isStatic = true } // 源 specRepos { url("https://git.haier.net/uplus/shell/cocoapods/Specs.git") } // IOS 引入其它模块 pod("FMDB") { version = "~> 2.7.5" } pod("uplog") { source = git("https://git.haier.net/uplus/ios/uplog.git") { branch = "kmm_1.7.1" } } } sourceSets { val commonMain by getting { dependencies { implementation(libs.kotlinx.coroutines.core) } } val androidMain by getting { dependencies { api("com.haier.uhome:UpLog:3.6.0") api("com.haier.uhome:uplog-core:3.4.0") } } val iosMain by getting { dependencies { } } } } ```
-
-
androidApp:通常作为库开发的 demo,是一个安卓模块
-
iosApp:通常作为库开发的 demo,是一个 xcode 工程
-
-
4. 代码说明
在 kmm 共享模块中
-
src/commonMain 是共享代码
-
src/androidMain 是安卓差异化代码,可以调用 JDK、AndroidSDK 中的 API
-
src/iosMain 是苹果手机的差异化代码,可以调用 IOS 平台的 API
4.1 举例
我们基于现有的日志库,实现一个跨平台的日志接口:安卓和 IOS 平台有各自的 UpLog 库,通过使用 Kotlin 的 expect 和 actual 关键字来实现差异代码和通用逻辑
-
commonMain:
kotlininternal expect object UpLog { fun d(tag: String, msg: String, vararg args: Any) fun d(tag: String, msg: String, t: Throwable) fun i(tag: String, msg: String, vararg args: Any) fun i(tag: String, msg: String, t: Throwable) fun w(tag: String, msg: String, vararg args: Any) fun w(tag: String, msg: String, t: Throwable) fun e(tag: String, msg: String, vararg args: Any) fun e(tag: String, msg: String, t: Throwable) } ```
-
androidMain:
- 这里我们可以发现,使用了 UpLoggerManager,其本质上是 UpLog 库中的 API
kotlininternal actual object UpLog { private const val LOGGER_NAME = "UpStorage" private val initialized = AtomicBoolean(false) private lateinit var logger: Logger actual fun d(tag: String, msg: String, vararg args: Any) { logger.debug("$tag: $msg", *args) } actual fun d(tag: String, msg: String, t: Throwable) { logger.debug("$tag: $msg", t) } actual fun i(tag: String, msg: String, vararg args: Any) { logger.info("$tag: $msg", *args) } actual fun i(tag: String, msg: String, t: Throwable) { logger.info("$tag: $msg", t) } actual fun w(tag: String, msg: String, vararg args: Any) { logger.warn("$tag: $msg", *args) } actual fun w(tag: String, msg: String, t: Throwable) { logger.warn("$tag: $msg", t) } actual fun e(tag: String, msg: String, vararg args: Any) { logger.error("$tag: $msg", *args) } actual fun e(tag: String, msg: String, t: Throwable) { logger.error("$tag: $msg", t) } init { if (initialized.compareAndSet(false, true)) { logger = UpLoggerManager.getInstance().createLogger(LOGGER_NAME) } } } ```
-
iosMain:
- 这里使用的是 cocoapods 生成库中的 iOS api
kotlinpackage com.haier.uplus.kmm.storage.platform import cocoapods.uplog.* internal actual object UpLog { private const val MODULE_NAME = "KmmStorage" private val logger = UPLog.getInstance()!!.createLogger(MODULE_NAME)!! actual fun d(tag: String, msg: String, vararg args: Any) = logger.logWithLevel(UPLogLevelDebug, msg) actual fun d(tag: String, msg: String, t: Throwable) = logger.logWithLevel(UPLogLevelDebug, msg) actual fun i(tag: String, msg: String, vararg args: Any) = logger.logWithLevel(UPLogLevelInfo, msg) actual fun i(tag: String, msg: String, t: Throwable) = logger.logWithLevel(UPLogLevelInfo, msg) actual fun w(tag: String, msg: String, vararg args: Any) = logger.logWithLevel(UPLogLevelWarning, msg) actual fun w(tag: String, msg: String, t: Throwable) = logger.logWithLevel(UPLogLevelWarning, msg) actual fun e(tag: String, msg: String, vararg args: Any) = logger.logWithLevel(UPLogLevelError, msg) actual fun e(tag: String, msg: String, t: Throwable) = logger.logWithLevel(UPLogLevelError, msg) } ```
如果要使用这个 UpLog 单例,不管在 common 中,还是在 android/ios 中都可以直接引用并调用
4.2 基本原理
Kotlin Native是一种将Kotlin源码编译成不需要任何VM支持的目标平台二进制数据的技术,编译后的二进制数据可以直接运行在目标平台上,它主要包含一个基于LLVM的后端编译器的和一个Kotlin本地运行时库。设计Kotlin Native的目的是为了支持在非JVM环境下进行编程,如在嵌入式平台和iOS环境下,如此一来,Kotlin就可以运行在非JVM平台环境下。
Kotlin Native 内部使用 cinterop 来对 Apple Framework 进行扫描,根据其头文件(.h)获取可以调用的类、方法、变量、常量以及他们的类型,最终生成 klib 文件。而 klib 文件中含着针对不同 CPU 架构所编译的二进制文件,以及可供 Kotlin Native 调用的 knm 文件, knm 文件类似 Jar 包中的 。class 文件,是被编译后的 Kotlin 代码,内部将 cinterop 扫描出来 的 Objective-C 内容转换成了 Kotlin 对应的内容,以便 IDE 可以进行索引,最终在 KMM 模块中使用 Kotlin 代码进行调用。
4.3 模块编译
-
编译命令:
./gradlew clean assemble
-
Android 和 iOS 的产物如下所示
- 可使用
maven-publish
插件把 kmm 模块的产物上传到 maven 私服:包括安卓端的 aar 文件和 iOS 各平台的 klib 文件
diff
- 如果单独要把 Framework 导出给 iOS 使用,把编译产物手动上传到 pod 私服即可
5. 可能碰到的问题
-
使用 pod 引入 iOS 模块的时候,出现找不到头文件的错误,例如:
php> Task :kmmStorageShared:cinteropUplogIosSimulatorArm64 Exception in thread "main" java.lang.Error: /Users/liuqing.yang/work/haier/kmm/ UpStorage/kmmStorageShared/build/cocoapods/synthetic/ios/build/ Release-iphonesimulator/uplog/uplog.framework/Headers/UPLogUpload.h:9:9: fatal error: 'UpLogUploadFileDelegate.h' file not found at org.jetbrains.kotlin.native.interop.indexer.ModuleSupportKt.getModulesASTFiles(ModuleSupport.kt:74) at org.jetbrains.kotlin.native.interop.indexer.ModuleSupportKt.getModulesInfo(ModuleSupport.kt:14) at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.buildNativeLibrary(main.kt:563) at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.processCLib(main.kt:317) at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.processCLibSafe(main.kt:242) at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.access$processCLibSafe(main.kt:1) at org.jetbrains.kotlin.native.interop.gen.jvm.Interop.interop(main.kt:100) at org.jetbrains.kotlin.cli.utilities.InteropCompilerKt.invokeInterop(InteropCompiler.kt:45) at org.jetbrains.kotlin.cli.utilities.MainKt.mainImpl(main.kt:23) at org.jetbrains.kotlin.cli.utilities.MainKt.main(main.kt:45) ``` - 或者提示 klib 不存在,例如: - ``` :kmmStorageShared:iosArm64Main: cinterop file: /Users/liuqing.yang/work/haier/kmm/UpStorage/kmmStorageShared/build/classes/kotlin/iosArm64/main/cinterop/kmmStorageShared-cinterop-uplog.klib does not exist ``` - 如果出现 klib 不存在,需要删除工程中的 ` .gradle `文件夹,然后重新 sync 工程就会看到找不到头文件的真实原因 - 最后根据提示,打开 IOS 工程,修复对应错误即可
-
根据官方的描述,纯的 Swift 模块,目前还不支持双向互操作,Swift 调用 Kotlin 没有问题。
Kotlin/Native 与 Objective-C 支持双向互操作。见 Interoperability with Swift/Objective-C | Kotlin
-
安卓 Kotlin 版本不一致问题
- 测试 Demo 的 Kotlin 版本目前是1.3.61,然而 Kmm 工程的版本为1.9.10
- 可能会碰到如下错误:
javascripte: Incompatible classes were found in dependencies. e: /Users/liuqing.yang/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.9.10/dafaf2c27f27c09220cee312df10917d9a5d97ce/kotlin-stdlib-common-1.9.10.jar!/META-INF/kotlin-stdlib-common.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0 ``` - 升级 Demo 中的 Kotlin 版本到 1.9.10,可能会碰到: - ``` > java.lang.NoClassDefFoundError: org/jetbrains/kotlin/gradle/plugin/KotlinBasePlugin ```
因为 Demo 工程的 AGP 版本太低,建议升级到 4.2.2 或更新,另外 gradle 版本升级到 7.2 或更新
另外如果提示
kotlin
Class 'xxxxxxxx' is compiled by a pre-release version of Kotlin and cannot be loaded by this version of the compiler
在工程根目录的 gradle.properties 中加入
ini
kotlin.experimental.tryK2=true
- 新的 Kotlin 版本中,kotlin-android-extensions 已移除,建议使用 viewBinding
arduino
android {
buildFeatures {
viewBinding true
}
}
6. 单元测试
6.1 集成 Cucumber
Setup
- 依赖导入
scss
dependencies {
androidTestImplementation(libs.androidx.test.core.ktx)
androidTestImplementation(libs.androidx.test.rules)
androidTestUtil(libs.androidx.test.orchestrator)
androidTestImplementation(libs.test.cucumber.android)
}
- KmmAndroidJUnitRunner
kotlin
package com.haier.uplus.kmm.storage.android.test
import android.os.Bundle
import io.cucumber.android.runner.CucumberAndroidJUnitRunner
import io.cucumber.junit.CucumberOptions
import java.io.File
/**
* Created by liuqing.yang
* 2023/9/11.
*/
@CucumberOptions(
features = ["features"],
strict = true,
)
class KmmAndroidJUnitRunner : CucumberAndroidJUnitRunner() {
override fun onCreate(bundle: Bundle?) {
bundle?.putString("plugin", getPluginConfigurationString())
//it crashes on Android R without it
File(getAbsoluteFilesPath()).mkdirs()
super.onCreate(bundle)
}
/**
* Since we want to checkout the external storage directory programmatically, we create the plugin configuration
* here, instead of the {@link CucumberOptions} annotation.
*
* @return the plugin string for the configuration, which contains XML, HTML and JSON paths
*/
@Suppress("SameParameterValue")
private fun getPluginConfigurationString(): String {
val cucumber = "cucumber"
val separator = "--"
return "junit:" + getCucumberXml(cucumber) + separator +
"html:" + getCucumberHtml(cucumber)
}
@Suppress("SameParameterValue")
private fun getCucumberHtml(cucumber: String) = "${getAbsoluteFilesPath()}/$cucumber/.html"
@Suppress("SameParameterValue")
private fun getCucumberXml(cucumber: String) = "${getAbsoluteFilesPath()}/$cucumber/.xml"
private fun getAbsoluteFilesPath() =
File(targetContext.getExternalFilesDir(null), "reports").absolutePath
}
- Feature 文件
vbnet
Feature: Kmm storage
Scenario Outline: auto insert some data to db
Given insertKeyValue"<Key>""<Value>"
Then insertSuccess"true"
Examples:
| Key | Value |
| a | 1 |
| b | 2 |
| c | 3 |
- Steps
kotlin
package com.haier.uplus.kmm.storage.android.test
import android.util.Log
import com.haier.uplus.kmm.storage.manager.UpStorage
import io.cucumber.java.en.Given
import io.cucumber.java.en.Then
import org.junit.Assert
/**
* Created by liuqing.yang
* 2023/9/11.
*/
class StorageStep {
companion object {
private const val TAG = "StorageStep"
}
@Volatile
private var insertRet: Boolean = false
@Given("insertKeyValue{string}{string}")
fun insertkeyvalue(key: String, value: String) {
insertRet = UpStorage.putIntValue(key, value.toInt())
Log.d(TAG, "insertkeyvalue: $insertRet")
}
@Then("insertSuccess{string}")
fun insertSuccess(ret: String) {
Log.d(TAG, "insertSuccess: $insertRet, $ret")
Assert.assertEquals(ret.toBoolean(), insertRet)
}
}
Running the tests
使用 Android Studio IDE
- Run > Edit Configurations
- 点击
+
按钮,选择 Android Instrumented Tests - 指定测试名称,选择测试模块,点击 OK,最后点击运行按钮
执行结果和日志可在 build 目录中查看
6.2 集成 Jacoco
- 引入 jacoco 插件
bash
plugins {
id("jacoco")
}
- 自定义扫描的源码目录和 class 文件目录
scss
tasks.register(kmmJacoco, JacocoReport::class.java) {
group = reporting
description = jacocoDesc
dependsOn(createDebugAndroidTestCoverageReport)
reports {
xml.enabled = true
html.enabled = true
}
def coverageClassDirs = fileTree(
//检测覆盖率的class所在目录(以项目class所在目录为准)
dir: '../../UpBluetoothPlugin/build/intermediates/javac/debug',
//增加以上目录中不需要检测的文件列表
excludes: [
'**/BuildConfig.class',
'**/impl/**.class'
]
)
getClassDirectories().setFrom(coverageClassDirs)
getSourceDirectories().setFrom(files(coverageSourceDirs))
File ecFile = new File("$buildDir/outputs/code_coverage/debugAndroidTest/connected");
ecFile.listFiles().each {
println it.name
getExecutionData().setFrom(files(it))
}
}
-
执行结果:
- HTML:
build/reports/jacoco/kmmJacoco/html
- XML:
build/reports/jacoco/kmmJacoco/kmmJacoco.xml
- HTML:
7. 团队介绍
「三翼鸟数字化技术平台-智家APP平台」 通过持续迭代演进移动端一站式接入平台为三翼鸟APP、智家APP等多个APP提供基础运行框架、系统通用能力API、日志、网络访问、页面路由、动态化框架、UI组件库等移动端开发通用基础设施;通过Z·ONE平台为三翼鸟子领域提供项目管理和技术实践支撑能力,完成从代码托管、CI/CD系统、业务发布、线上实时监控等Devops与工程效能基础设施搭建。