什么是KMM?
KMM 即 Kotlin Multiplatform Mobile 是一个 SDK,旨在简化跨平台移动应用程序的开发。通过 KMM 开发者可以在 iOS 和 Android 应用程序之间共享通用代码,并仅在必要时编写特定于平台的代码。
KMM并不会替代 Android 和 iOS 的原生开发, 而是提倡将共有的逻辑部分抽出,由 KMM 封装成 Android(Kotlin/JVM) 的 aar 和 iOS(Kotlin/Native) 的 framework ,再提供给 View 层进行调用,从而节约一部分的工作量。
KMM 的优势
-
无需内置多套引擎(runtime),包体积增量更少 。
-
对于 Android 开发者无需多学习一套编程语言和编程思想,门槛更低 。
-
基于双端标准组件输出,审核被拒风险较小(iOS)。
-
更强的互操作性, 支持与本地编程语言的双向互操作,可以直接使用现有库,避免了众多基础组件的重复建设。
环境配置
这里假设,你有 android 开发基础并且已经安装了高版本的 AndroidStudio(这里不会介绍 Xcode 的配置)。
在 AndroidStudio 中搜索插件 Kotlin Multiplatform Mobile 并安装,如下图所示:(我这边是已经装好了)
创建项目
安装好插件并重启后,我们可以创建一个 Kotlin Multiplatform App,如下图所示:
项目结构
Kotlin Multiplatform App(左)、 Kotlin Multiplatform Libray(右)
androidApp、iosApp 就是对应的 Android、iOS 代码库,shared 为共享逻辑模块,即存放 Android、iOS 公共业务逻辑的部分。
我们看看根目录的settings.gradle.kts
文件都做了什么:
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "My_Application"
include(":androidApp")
include(":shared")
主项目只 include 了 androidApp 和 shared 这两个子项目,因为这两个项目是Gradle项目,而 iOS 作为 Xcode 项目,储存在根项目的另一个文件夹。Xcode 有自己的编译系统,因此 iOS 项目并不依靠 Gradle 去和共享工程建立联系,而是依靠将共享工程打包成 framework 供 iOS 项目使用。
commonMain 为公共模块,该模块的代码与平台无关,是通过 expecteed 关键字对一些 api 的声明(声明的实现在 platfrom module 中)。
androidMain 和 iosMain 分别为Android 和 ios 相关的生态系统常规库,通过 actual 关键字在平台模块进行具体实现。
我们继续看看shared
模块的gradle文件都做了什么:
plugins {
kotlin("multiplatform")//意味着引入KMM 插件。
id("com.android.library")//意味着生成一个Android aar,其配置用android {}进行了包裹
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
kotlin {
targetHierarchy.default()
android {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
//iOS framework是使用Kotlin/Native进行编译的,相应的配置是用iosXXX{}进行了包裹
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
//定义了输出格式为framework,输出名称为shared。
it.binaries.framework {
baseName = "shared"
}
}
//支持分别引入implementation来实现各自的逻辑。另外Kotlin标准库会被自动加到相应的sourceSet中,无需重复引入。
sourceSets {
val commonMain by getting {
dependencies {
//put your multiplatform dependencies here
// implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
}
val androidMain by getting {
dependencies {
}
}
val iosMain by getting {
dependencies {
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
}
}
android {
namespace = "com.example.myapplication"
compileSdk = 33
defaultConfig {
minSdk = 24
}
}
运行程序
我们这里仅运行Android程序,运行结果如下图所示:
这个结果来自 shared 模块中 commonMain 下的 Greeting 文件的 greet 方法,代码如下所示:
class Greeting {
private val platform: Platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}
greet() 方法调用 platform.name, Platform 的实现如下:( Platform 是个接口,使用expect 关键字来声明 getPlatform(),再由 Android 和 iOS 通过使用 actual 关键字分别实现)
interface Platform {
val name: String
}
expect fun getPlatform(): Platform
这个 platform.name 的值在 android 手机调用时会取 androidMain 下的 Platform.android.kt 中的值,所以取出的是 android 手机的sdk版本,代码如下所示:
class AndroidPlatform : Platform {
override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}
actual fun getPlatform(): Platform = AndroidPlatform()
同一个方法在iOS的手机上运行则会显示iOS版本号,因为取的是 iosMain 下的 Platform.ios.kt 中的值,代码如下所示:
class IOSPlatform: Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
actual fun getPlatform(): Platform = IOSPlatform()
最后由 androidApp和iosApp用各自的调用。这里只说 android 端。androidApp 下的 MainActivity.kt 中使用 Greeting.greet() 方法获取然后显示,代码如下所示:
至于为什么 activity 的写法和之前差距很大,那是因为这里默认使用的是 kotlin+compose 的写法,一种不需要xml的布局写法...这里就先不细说了。
expect/actual 机制
KMM 里 expect/actual 机制是非常重要的,因为它提供了一种语法技术来解决平台相关的问题。举例来说,当我们需要在业务逻辑中使用设备的 model 号时,我们需要编写特定平台的代码才能实现。这时,expect 就相当于声明一个协议,它定义了我们期望得到的接口或数据,之后各个平台都需要独立地实现这个协议来满足业务需要。
基本流程如下: 在 commonMain 目录下建立一个 expect 类或 Top-Level 方法 , 类似创建一个协议声明。分别在 androidMain 和 iosMain 目录中,创建与 expect 声明(类名、方法名、参数类型及名称)完全一致的实现,否则编译器会报错。