在移动开发领域,Android 和 iOS 版本的应用程序通常会有很多共同点,背后的业务逻辑基本也是一致的。文件下载,读写数据库,从远程服务器获取数据,解析远程数据等等。所以我们为什么不只写一次业务逻辑代码,在不同的平台上共享呢?
有了这个想法之后,Jetbrains 带来了 Kotlin Multiplatform Project 。
➡️ 什么是 Kotlin Multiplatform Mobile?
Kotlin Multiplatform Mobile (KMM) 是由 Jetbrains 提供的跨平台移动开发 SDK 。借助 Kotlin 的 跨平台能力,你可以使用一个工程为多个平台编译。
使用 KMM,具备灵活性的同时也保留了原生编程的优势。为 Android/iOS 应用程序的业务逻辑代码使用单一的代码库,仅在需要的时候编写平台特定代码,例如实现原生的 UI,使用平台特定 API 等等。
KMM 可以和你的工程无缝集成。共享代码,使用 Kotlin 编写,使用 Kotlin/JVM 编译成 JVM 字节码,使用 Kotlin/Native 编译成二进制,所以你可以和使用其他一般类库一样使用 KMM 业务逻辑模块。
在写这篇博客的同时,KMM 仍然处于 Alpha,你可以开始尝试在你的应用中共享业务逻辑代码。
在移动开发领域,KMM 目前没有为大众所熟知。Jetbrains 开发了 Android Studio 的 KMM 插件 来帮助你快速设置 KMM 工程。插件还可以帮助你编写,运行,测试共享代码。
➡️ 一步一步构建 HELLO WORLD KMM 应用
- 在 Android Studio 上安装 Kotlin Multiplatform Mobile 插件。打开 Android Studio -> 点击 Configure -> 选择 Plugins
- 在 plugins 部分选择 Marketplace ,搜索 KMM,安装并重启 Android Studio。
- 在 Android Studio 首页选择 “Start a new Android Studio project” 。
- 在 “Select a project Template” 页面,选择 “KMM Application” 。
- 设置工程名称,最低 SDK,文件目录,包名等。
现在,你需要等待工程的第一次构建,需要花费一些时间去下载和设置必要的组件。
译者注:KMM 插件要求你的 Kotlin 插件版本至少为 4.0 版本以上
➡️ 运行你的程序
在菜单栏选择你要运行的平台,选择设备,点击 Run
要运行 iOS 应用,你需要安装 Xcode 和模拟器。
➡️ 瞅一眼代码
Android 开发者? 看起来很熟悉? 😎
IOS 开发者? 看起来就像外星人? 👽
➡️ 模块
- shared 模块 —— 存放 Android/iOS 通用业务逻辑代码的 Kotlin 模块,会被编译为 Android library 和 iOS framework。使用 Gradle 进行构建。
- androidApp 模块 —— Android 应用的 Kotlin 模块。使用 Gradle 构建。
- iosApp 模块 —— 构建 iOS 应用的 Xcode 工程。
Project 的 build.gradle.kts 文件:
buildscript { repositories { gradlePluginPortal() jcenter() google() mavenCentral() } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10") classpath("com.android.tools.build:gradle:4.0.1") } } group = "com.aman.helloworldkmm" version = "1.0-SNAPSHOT" repositories { mavenCentral() } 复制代码
➡️ Shared module
shared 模块包含了Android 和 iOS 的公用代码。但是,为了在 Android/iOS 上实现同样的逻辑,有时候你不得不写两份版本特定代码,例如蓝牙,Wifi 等等。为了处理这种情况,Kotlin 提供了 expect/actual 机制。shared 模块的源代码按三个源集进行分类:
commonMain
下存储为所有平台工作的代码,包括expect
声明androidMain
下存储 Android 的特定代码,包括actual
实现iosMain
下存储 iOS 的特定代码,包括actual
实现
每一个源集都有自己的依赖,Kotlin 标准库依赖会自动添加到所有源集,你不需要在编译脚本中声明。
build.gradle.kts
这份 build.gradle.kts 文件包含了 shared 模块对于 Android/iOS 的配置。
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { kotlin("multiplatform") id("com.android.library") id("kotlin-android-extensions") } group = "com.aman.helloworldkmm" version = "1.0-SNAPSHOT" repositories { gradlePluginPortal() google() jcenter() mavenCentral() } kotlin { android() ios { binaries { framework { baseName = "shared" } } } sourceSets { val commonMain by getting val commonTest by getting { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) } } val androidMain by getting { dependencies { implementation("com.google.android.material:material:1.2.0") } } val androidTest by getting { dependencies { implementation(kotlin("test-junit")) implementation("junit:junit:4.12") } } val iosMain by getting val iosTest by getting } } android { compileSdkVersion(29) sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { minSdkVersion(24) targetSdkVersion(29) versionCode = 1 versionName = "1.0" } buildTypes { getByName("release") { isMinifyEnabled = false } } } val packForXcode by tasks.creating(Sync::class) { group = "build" val mode = System.getenv("CONFIGURATION") ?: "DEBUG" val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator" val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64" val framework = kotlin.targets.getByName<KotlinNativeTarget>(targetName).binaries.getFramework(mode) inputs.property("mode", mode) dependsOn(framework.linkTask) val targetDir = File(buildDir, "xcode-frameworks") from({ framework.outputDirectory }) into(targetDir) } tasks.getByName("build").dependsOn(packForXcode) 复制代码
androidApp 模块的 build.gradle.kts 文件
plugins { id("com.android.application") kotlin("android") id("kotlin-android-extensions") } group = "com.aman.helloworldkmm" version = "1.0-SNAPSHOT" repositories { gradlePluginPortal() google() jcenter() mavenCentral() } dependencies { implementation(project(":shared")) implementation("com.google.android.material:material:1.2.0") implementation("androidx.appcompat:appcompat:1.2.0") implementation("androidx.constraintlayout:constraintlayout:1.1.3") } android { compileSdkVersion(29) defaultConfig { applicationId = "com.aman.helloworldkmm.androidApp" minSdkVersion(24) targetSdkVersion(29) versionCode = 1 versionName = "1.0" } buildTypes { getByName("release") { isMinifyEnabled = false } } } 复制代码
➡️ 使用 Expect/Actual 关键字
对于跨平台应用来说,版本特定代码是很常见的。例如你可能想知道你的应用是运行在 Android 还是 iOS 设备,并且得到设备的具体型号。为了完成这个功能,你需要使用 expect/actual 关键字。
首先,在 common 模块中使用 expect
关键字声明一个空的类或函数,就像创建接口或者抽象类一样。然后,在所有的其他模块中编写平台特定代码来实现对应的类或函数,并用 actual
修饰。
注意,如果你使用了
expect
,你必须提供对应名称的actual
实现。
否则,你会得到如下错误:
➡️ Expect/Actual 的使用
commonMain
expect class Platform() { val platform: String } 复制代码
androidMain
actual class Platform actual constructor() { actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}" } 复制代码
iosMain
import platform.UIKit.UIDevice actual class Platform { actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion } 复制代码
MainActivity.kt (Android)
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val tv: TextView = findViewById(R.id.text_view) tv.text = "Hello World, ${Platform().platform}!" } } 复制代码
ContentView.swift (iOS)
struct ContentView: View { var body: some View { Text("Hello World, "+ Platform().platform) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } 复制代码
恭喜!! 你已经完成了你的第一个 KMM app 。
➡️开源 KMM 应用
➡️ 可用的 KMM 类库
AAkira/Kotlin-Multiplatform-Libraries
译者说
在已经一片红海的移动端跨平台开发领域,Kotlin 另辟蹊径,让你可以继续使用平台原生方式开发 UI,在业务逻辑上做到 “Write once,run everywhere”。甚至放飞一下自我,未来的某一天是不是可以用 Flutter 做 UI 上的通用,用 Kotlin 做业务逻辑上的通用?
不管怎样,最终还是得开发者买账才行。不知道你怎么看 KMM,在评论区留下的你的看法吧!