ITPub博客

首页 > 移动开发 > ANDROID > 从Android到Kotlin Multiplatform:我们的模块化业务逻辑

从Android到Kotlin Multiplatform:我们的模块化业务逻辑

原创 ANDROID 作者:山有木xi 时间:2025-08-02 10:41:13 0 删除 编辑

昨天看到鸿蒙设备正式突破 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/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
Oracle OCA(Java),IBM高级工程师认证,中国软件行业人才(高级c语言),高级Android工程师,对数据库并发与性能调优也有一定了解

注册时间:2019-04-25

  • 博文量
    147
  • 访问量
    390756