kotlin multiplatform mobile 初探
kotlin multiplatform mobile programing 的探索
本文是一篇浅显的kmm 学习总结,希望对kotlin multiplatform mobile(下面简称kmm)的功能、支持平台、适用场景、使用方法、优缺点等有一个初步的认知和理解。
一、什么是kmm?
1.1 引用9月15日 kmm发布alpha版本时的一句话:
Kotlin Multiplatform Mobile (KMM) 是一个 SDK,允许您在 iOS 和 Android 应用程序中使用相同的业务逻辑代码。
两个关键字:SDK、共享业务逻辑代码
1.2 详细描述
应用程序的 Android 和 iOS 版本通常有很多共同点,但也会有很大不同 – 尤其在 UI 方面。
同时,应用程序的业务逻辑,包括数据管理、分析和认证等功能,往往是相同的。
因此,标准操作是跨平台共享应用程序的某些部分,同时保持其他部分完全相互独立。
通过 KMM,您可以获得这种灵活性,并保留原生编程的优势。
将单个代码库用于 iOS 和 Android 应用的业务逻辑,只在实施原生 UI 有必要时或使用平台特定 API 时编写平台特定代码。
在官方提供的获取平台版本号、进行算数运算、网络数据I/O的案例可以看出:
(1). 向不同的平台提供一致的api接口;
(2). 共享的业务逻辑统一由kotlin编写,android直接使用,而iOS作为framework依赖为静态库后通过编写swift代码,调用暴露的API使用
(3). 各自的UI由各自实现
二、km支持的平台
2.1 支持的平台
Platform | preset | comments |
---|---|---|
kotlin/jvm | jvm | |
kotlin/js | js | Select the execution environment: – browser {} for applications running in the browser. – nodejs{} for applications running on Node.js. |
Android application/library | androidNativeArm32 androidNativeArm64 | Manually apply an Android Gradle plugin – com.android.application or – com.android.library. You can only create one Android target per Gradle subproject. |
Android NDK | androidNativeArm32 androidNativeArm64 | The 64-bit target requires a Linux or macOS host. You can build the 32-bit target on any supported host. |
iOS | iosArm32 / iosArm64 iosX64 | Requires a macOS host. |
tvOS | tvOSArm64 / tvOSX64 | |
watchOS | watchosArm32 / watchosArm64 / watchosX86 | |
macOS | macosX64 | Requires a macOS host. |
Linux | linuxArm64 / linuxArm32Hfp linuxMips32 / linuxMipse32 linuxX64 | Linux MIPS targets (linuxMips32 and linuxMipsel32) require a Linux host. You can build other Linux targets on any supported host. |
Windows | mingwX64 / mingwX86 | Requires a Windows host. |
WebAssembly | wasm32 |
支持的平台比较多,几乎覆盖了市场所有主流平台
2.2 工程结构
Project:
|-- androidApp: 安卓应用工程文件
|-- iosApp: iOS应用工程文件
|-- shared: 共享代码文件
|-- …
|-- commonMain: 共享逻辑
|-- androidMain: 需要由android实现的expect
|-- iosMain: 需要由ios实现的expect的kotlin代码
|-- commonTest: 多平台测试
|-- androidTest:
|-- iosTest:
|-- …
三、kmm的基本原理
3.1 Kotlin Native
Kotlin/Native 是一种将 Kotlin 代码编译为无需虚拟机就可运行的原生二进制文件的技术。 它是一个基于 LLVM 的 Kotlin 编译器后端以及 Kotlin 标准库的原生实现。
kotlin/Native 弥补了无需虚拟机或者运行时的自包含程序的使用场景,这是什么意思呢?因为从Android Mobile Application的角度来看问题,Android 应用时运行在JVM虚拟机上的,kotlin代码在Android平台上最终也会编译成jar的形式在JVM中执行,并且有Android Runtime的概念,而iOS是没有这些概念的,可以理解为kotlin/Native 在移动开发上是为了适应iOS编程的一种技术实现。
具体有些什么?
a. 支持那些?
似乎和kmm所能支持的一样,因为是由kn实现的,所以一样
b. 互操作:
kotlin/Native 可以
- 用于多个平台的可执行文件
- 用于 C/C++ 项目的静态库或动态库以及 C 语言头文件
- 用于Swift 与 Objective-C 项目的 Apple Framework
// shared/build.gradle.kts
plugins {
kotlin("multiplatform")
}
kotlin {
android()
ios() {
binaries {
framework {
baseName = "shared"
}
}
}
}
在工程中的build.gradle.kts 进行适当的配置,就可以将公共共享代码文件打包成framework供给iOS使用,并且在库(framework)中由.h 文件暴露API 方便在iOS工程中调用
此外,可以使用现有的
- 静态或动态 C 语言库
- C 语言、 Swift 以及 Objective-C 框架
使用cocoapods方式依赖一些现有的kotlin multiplatform library, 然后在iosMain中导入即可调用相应库中的API
支持从github、git、本地等方式pod,需要在xcode的profile中加入一些配置。
除了已经自动依赖的KN,目前可用的官方common lib:(主要是一些存储、SQL操作、协程和并发、网络、断言、数据序列化和反序列化)
plugins {
kotlin("multiplatform")
// id("org.jetbrains.kotlin.native.cocoapods")
kotlin("native.cocoapods")
}
version = "x.x"
kotlin {
...
ios()
cocoapods {
ios.deploymentTarget = "target version"
summary = "Some description for a Kotlin/Native module"
homepage = "Link to a Kotlin/Native module homepage"
pod("Cocoapods dependency name", "version", podspecFile, "Kotlin/Native module name")
}
...
}
3.2 LLVM
一种与虚拟机无关的支持任意编程语言的动态、静态编译的项目
3.3 expect/actual
在大部分引用第三方库的场景中,Android/iOS使用的文件、方式并不相同,或者一些系统API调用上有许多差异,在遇到这种差异时,需要使用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 constructor() {
actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
其中==“platform.UIKit.UIDevice”== 是由kotlin/Native 获取到的NSObject,而NSObject是一个object-c对象,实现了kotlin使用object-c库的方法。
最终,Kotlin 代码共享代码将转换为源代码(iOS 上的本地可执行代码,使用KN/LLVM) ,在iOS工程中作为framework,在 iOS 上运行。
第三方和项目本地封装的功能API需要通过在kotlin build.gradle.kts通过cocoapods配置,并导入实现在iosMain中调用,Android则照旧。
KN中包含的一些ios内容
CoreAudio
MediaPlayer
MessageUI
ModelIO
Network
OPENGL
Photos
PhotosUI
PushKit
QuickLook
UIKit …
举个🌰 :
// Kotlin/Native Photos.kt
public open external expect fun fetchCollectionsInCollectionList(
collectionList: platform.Photos.PHCollectionList,
options: platform.Photos.PHFetchOptions?
): platform.Photos.PHFetchResult { /* compiled code */ }
/*** 可以在iosMain中调用,根据指定的options(可以配置AssetSourceTypes, limit等)获取图片文件集合
* 而androidMain仍然使用contentProvider获取
* 上述作为两个actual fun,而在commonMain中定义一个expect fun
*/
// shared/commonMain/MediaLibrary.kt
expect class MediaLibrary {
fun fetchPhotosCollection()
fun updateSelectState(...)
fun productThumbnails(...)
}
// iosMain/MediaLibrary.kt
import platform.Photos.PHCollection
actual class MediaLibrary {
actual fun fetchPhotosCollection() {
PHCollection.fetchCollectionsInCollectionList(collection, options)
...
}
...
}
// androidMain/MediaLibrary.kt
actual class MediaLibrary {
actual fun fetchPhotosCollection() {
val cursor: Cursor = contentResolver.query(uri, selection, selectionArgs, sortBy)
...
}
...
}
于是乎,
- 获取系统相册、压缩解码或产生缩略图、图片选择状态、等逻辑都可以统一在commonMain中声明为expect;
- 在androidMain/iosMain中实现actual;
- 在androidApp/iosApp各自编写Recyclerview/UICollectionView
- 在androidApp/iosApp调用commonMain提供的expect fetchPhotosCollection方法获取数据(ios实质是调用的shared.framework中的.h文件声明的同名方法)
四、比较
4.1 自身设计思想导致的优缺点
Pros:
- 节省开发事件和费用,主要是指的复用业务逻辑这一部分,不用在Android/iOS写两套业务逻辑;
- 对已有工程无需重构可以分部分、分步骤接入
- 学习成本少,kotlin代码与swift相似,对于iOS开发人员学习也比较容易,与Java完全可相互操作;
- 完全使用各自平台的特性,没有额外的性能开销;
- 支持双平台调试。
Cons:
- 可以shared的编码和通用库、插件比较少;
- kotlin multiplatform 工程配置(gradle、plugin主要是)比较繁琐,并且这方面的专家较少;
- 目前还处于alpha版本,有许多api还在设计变动阶段,稳定性不足;
- 多线程编程不足,kotlin协程iOS完全不支持;
- iOS调试方面有许多问题,在多数情况下可能仍然是使用XCode更优。
4.2 与React Native的比较
- RN具有完整的生态;
- RN在插件、npm库方面具有较大优势;
- RN既可以复用一些常用的UI代码,也可以复用部分业务逻辑,并具有双端样式统一的效果;
- RN的复用UI需要生成AndroidBundle或JSBundle文件,在双端运行,在Android上增加了额外的性能开销,而KMM各自使用原生特性在性能方面更优;
- KMM在业务逻辑和工程架构上具有逻辑清晰,层次分离的效果,对于MVP项目,完全可以把P层、M层全部在shared中实现
- 没有桥接这一类概念,在Android中直接调用shared的API,在iOS中也是直接调用shared.framework头文件暴露的接口
五、Demo:
编写了一个获取相册的图片的demo。
基本结构是:
MultiplatGallery:
|-- shared
| |-- commonMain
| |-- MLocalMediaManager.kt
| |-- androidMain
| |-- MLocalMediaManager.kt
| |-- iosMain
| |-- MLocalMediaManager.kt
|
|-- androidApp
|-- iosApp
expect class MLocalMediaManager() {
fun fetchMediaCollection(isVideo: Boolean): String?
}
iOSMain
actual fun fetchMediaCollection(isVideo: Boolean): String? {
var result: String? = null
val options = PHFetchOptions.alloc()
options?.apply {
fetchLimit = 60uL
predicate = NSPredicate.predicateWithFormat(
"mediaType == %ld",
if (isVideo) PHAssetMediaTypeVideo else PHAssetMediaTypeImage)
val sortDescriptor = NSSortDescriptor
.sortDescriptorWithKey("creationDate", false)
sortDescriptors = listOf(sortDescriptor)
}
val album = PHAssetCollection.fetchAssetCollectionsWithType(
PHAssetCollectionTypeAlbum,
PHAssetCollectionSubtypeAny,
null
)
val albumCollection = PHAssetCollection.fetchTopLevelUserCollectionsWithOptions(null)
val count = albumCollection.count.toString()
result = "相册对象:${albumCollection.firstObject.toString()}\n\n 相册个数:${count}个\n"
if (albumCollection.firstObject !is PHAssetCollection) return result
val l =
PHAsset.fetchAssetsInAssetCollection(albumCollection.firstObject as PHAssetCollection, options)
if (l.count <= 0uL) result += " $l\n\n"
for (i in 0uL until l.count) {
result += "图片对象:${l.objectAtIndex(i)}\n\n"
}
return result
}
iOS Swift
import shared
func load() -> String {
return MLocalMediaManager().fetchMediaCollection(isVideo: false)
}
...
// call load function
struct ContentView: View {
var body: some View {
Text(load())
}
}