昨天看到鸿蒙设备正式突破 1000 万,其实在鸿蒙 Next 还在内测的时候,我们就参与了鸿蒙的生态建设,推进我们的产品鸿蒙化,当时还上过 2024 年华为开发者大会的宣传。未来也差不多明确了一个项目必须要同时维护三个平台,着实压力不小。可以说 Harmony 也是我们下定决心要使用跨平台技术接入的“最后一根稻草”。
多端同步的问题
其实我们一直有一个很头疼的问题,因为项目的起步时间和公司内部技术栈的情况,我们的所有项目几乎都是 Android 已经发展了一段时间,已经有了一定的用户体量后,才会推出 iOS 端。 iOS 端一“出生”就面临着必须追赶 Android 端的脚步实现已有的功能、也必须迎接市场的挑战去做新的功能。
这也导致了我们的项目在不同的端的进度一直没有办法对齐, Android 端 1 年前就有的功能, iOS 端可能一直没排上期。现在又多了一个平台,那也意味着三端的功能“一定”是对不齐的。而我们的鸿蒙端上线后收到的最多的反馈就是,“鸿蒙端怎么找不到 xx 功能”、“ xx 功能怎么没有 Android 好用”“什么时候上 xx 功能”,而我们负责鸿蒙的同事就算把键盘敲冒烟也不可能在短期内做到 Android 端的全部功能。
而同时在已有的 Android 和 iOS 项目中同步的功能也一直在出现问题,同一个业务逻辑, Android 和 iOS 处理时总会出现不一样的情况,以一个数据上传的功能举例, Android 端实现后, iOS 端做的时候发现接口的字段 iOS 需要额外包一层,所以需要做出修改,这个时候只能服务端开一个 v2 接口给 iOS 端使用。
这也意味着,我们必须做出适应时代和市场的改变。

在考虑和研究了很多跨平台方案后,因为团队技术栈和现有项目的因素,最终选择了 KMP 做为解决方案,实现以下目标:
1、 快速的同步我们项目的 Android 、 iOS 、 Harmony 端的功能,特别是 Android 端已经存在的功能,能够快速同步到其他端
2、 保持较高的性能
3、 统一业务逻辑,减少多端对齐带来的时间浪费
4、 以业务层为核心,指导 ui 层, ui 层只需要根据接收到的指令显示布局和交互即可
从最简单的模块开始
我们的项目中有一个意见反馈模块,这个模块的功能和页面是比较简单的,也是最容易迁移的模块之一,我们决定拿这个模块作为试水的模块之一,首先梳理一下将这个模块 kmp 化的思路
1、 意见反馈界面需要展示 qq 、邮箱、反馈类别等数据,需要集中管理这些数据由 kmp 下发
2、 需要封装用户反馈的文本和设备信息传递给服务端
3、 封装上传文件
4、 下发状态给 ui 层,指导 ui 展示“反馈成功”、“加载中”、“反馈失败”等交互
总结其实就数据和 ui 状态下发的处理
在项目中选择创建 module ,然后选择 kmp

不同的目录下对应不同的平台的特性代码,其中 commonMain 是公用的代码逻辑所在的地方

以 ui 状态下发举例子,在 commonMain 中先创建状态
sealed class FeedBackState {
object Init : FeedBackState()
object SubmitSuccess : FeedBackState()
object SubmitError : FeedBackState()
object SubmitLoading : FeedBackState()
}
然后创建控制中心
class FeedbackViewModel() {
private val _uiStateFlow = MutableStateFlow<FeedBackState>(FeedBackState.Init)
val uiStateFlow get() = _uiStateFlow
}
然后在相关界面的 ui 改为接收
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mFeedbackViewModel.uiStateFlow.collect { state ->
when (state) {
is FeedBackState.Init -> {
}
is FeedBackState.SubmitLoading -> {
}
is FeedBackState.SubmitSuccess -> {
}
is FeedBackState.SubmitError -> {
}
}
}
}
}
数据管理同样放入 FeedbackViewModel 中,网络请求改用 ktor ,创建 client 后就可以触发请求:
fun createDefaultClient(): HttpClient {
return HttpClient {
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
timber().d(message)
}
}
level = LogLevel.ALL // 可选:BODY / INFO / HEADERS / ALL
}
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
})
}
defaultRequest {
val headers = getPlatformHeaderProvider().getHeaders()//加入请求头
headers.forEach { (key, value) ->
header(key, value)
}
}
install(HttpTimeout) {
requestTimeoutMillis = 15_000
connectTimeoutMillis = 10_000
socketTimeoutMillis = 15_000
}
}
}
现在遇到个问题,如果我们需要用到平台特性代码,例如获取设备 id ,设备系统版本,公共的部分不支持,就可以使用 Kmp 提供另外一个能力:各平台自己实现相关能力
首先在 commonMain 中定义一个接口和一个 expect 方法,他会提示你需要在各个平台单独实现功能
interface PlatformInfoProvider {
fun getChannel(): String
fun getVersion(): String
fun getModel(): String
fun getNetwork(): String
}
expect fun getPlatformInfoProvider(): PlatformInfoProvider
以 Android 平台举例,在 androidMain 下自动生成 PlatformInfoProvider.android.kt 类,我们稍加改造 :
class AndroidPlatformInfoProvider() : PlatformInfoProvider {
override fun getChannel(): String = ChannelUtil.getChannel()
override fun getVersion(): String = “1.0.0”
override fun getModel(): String = "${Build.BRAND}-${Build.MODEL}"
override fun getNetwork(): String = getCellularOperatorType()
}
actual fun getPlatformInfoProvider(): PlatformInfoProvider = AndroidPlatformInfoProvider()
Flow 替代 liveData
我们原本的项目结构是 MVVM 结构的,在 ViewModel 中使用的是 liveData ,替换为 kmp 之后需要注意改用 FLow ,而 FLow 有两中, SateFlow 和 SharedFlow ,两者的区别如下:
|
特性 |
StateFlow |
SharedFlow |
|
是否有初始值 |
是,必须有 |
否,可以没有 |
|
是否会保存最新值 |
是 |
默认不会(可配置 replay) |
|
是否支持多个订阅者 |
是 |
是 |
|
是否粘性(新订阅立刻收到数据) |
是(立即收到最新值) |
(根据 replay 配置) |
|
是否适用于状态类 UI 数据 |
非常适合 |
更适合事件或一次性通知 |
|
是否替代 LiveData |
是(最接近 LiveData) |
更像 EventBus |
举个使用的例子:
当你需要传递重复的数据时,应该使用 StateFlow ,因为 SharedFlow 对于重复的数据会忽略,当然可以通过配置解决,但是还是建议使用官方推荐的方式。
在 Kmp 中可以这样调用
suspend fun submit(feedbackMessage: ModelFeedbackMessage) {
val messageString = Json {
encodeDefaults = true // 控制是否输出默认值
ignoreUnknownKeys = true
}.encodeToString(feedbackMessage)
val result = withContext(Dispatchers.IO) { comment(messageString) } // 其实kotlin更推荐切换flow实现切换上下文,但是我们快速实现所以不在
if (result.code == 200) {
_uiStateFlow.emit(FeedBackState.SubmitSuccess)
} else {
_uiStateFlow.emit(FeedBackState.SubmitError)
}
}
对于必须传入的平台特性的参数
有时候我们需要传入平台特性的参数,例如 Android 的 Context 作为参数,这个时候在 Kmp 中没有办法直接使用 Context ,有两种解决方法:
1、 使用 Any 作为参数类型,在具体的使用的地方转为真正的参数类型
// 使用Any expect fun getPlatformInfoProvider(context: Any): PlatformInfoProvider // 使用的地方强转 actual fun getPlatformInfoProvider(context: Any): PlatformInfoProvider = AndroidPlatformInfoProvider(context as Context)
2、 使用依赖注入
如果觉得强转的方法不够优雅, kotlin 也提供了解决方法,例如: Koin
// 定义接口
interface PlatformInfoProvider {
fun getPlatformName(): String
}
// 用 Koin 提供依赖
expect val platformInfoProvider: PlatformInfoProvider
// androidMain中的实现
actual val platformInfoProvider: PlatformInfoProvider
get() = AndroidPlatformInfoProvider()
// 模块化的方式注册到Koin
val androidModule = module {
single<PlatformInfoProvider> { AndroidPlatformInfoProvider() }
}
// Koin初始化,放在应用入口,例如Android的Application
startKoin {
modules(androidModule)
}
// 调用方式
val provider: PlatformInfoProvider by inject()
如何实现 kmp 运行在 Harmony 设备上
最开始我们在研究直接编译的可能性,参考了 b 站的做法,直到 6 月份,腾讯正式开源 Kuikly 框架,其中 ui 的部分其实还有一点不自由,但是作为业务层,可以支持我们导出为 Harmony 使用已经足够满足我们的需求了。
以上面的使用方式列举,可以将 Android 现有模块快速的封装为 kmp 模块。这也是我们本次客户端方面技术变革的核心目标,快速高效高性能的将 Android 业务逻辑以 kmp 共享出去,让各端专注于实现 Ui 方面的交互。
其他方案
我们也考虑过其他方案,例如 Flutter 、 uniappX 、 uniapp 、 C++/Rust ,但是都没有 kmp 更适合我们的团队。
Flutter : Flutter 的优势其实是 Ui 层的统一,而在跟我们已有的原生代码进行交互的化,就必须要经过序列化和反序列化数据的过程,这会带来不可避免的性能损失,而同时我们更希望只是将业务层进行共享, Flutter 的优势并不能完全体现出来。
Uniapp : uniapp 的优势是支持小程序,但是在性能上就显得有点鸡肋,特别是 uniapp 和原生之间的交互包括生命周期的管理非常恐怖。
UniappX :据 uniappX 团队描述 uniappX 是将代码翻译成各个平台的原生代码,但是目前为止似乎并没有达到真正可以商用的地步。
Rust :大部分使用 rust 的技术团队都面对着高性能高安全的需求,例如谷歌就用 rust 写了部分 Android 底层代码,但是对于业务需求来说 rust 的代码量会是 kmp 的数倍。 rust 的跨平台需要繁琐的 FFI ,同时 rust 和 kotlin 、 oc 、 swift 之间都需要桥接,同时 rust 的生态是面向系统编程,例如图形学、数据模型等对于业务常用的生命周期、网络状态可能并没有非常合适的库可以使用,也或许是我们没有找到。
面对三端并行开发带来的巨大复杂度,我们并不急于 “ 全栈统一 ” ,而是更关注架构的合理性与团队协作的可持续性。 Kotlin Multiplatform 为我们提供了一种相对平衡的解法:既不牺牲原生的性能与体验,也能实现核心逻辑的最大化复用。它本质上不是一种 “ 跨平台替代品 ” ,而是对多端架构设计能力的升级。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69917874/viewspace-3092635/ ,如需转载,请注明出处,否则将追究法律责任。