BikeHub 是一个运行在 Android、iOS 以及通过 WebAssembly 在浏览器上的 KMP 应用。大约 84% 的代码生活在 commonMain 中。在迁移过程中的任何时候,Android 应用都没有停止过运行。

这不是一个关于英雄式重写的神话故事。真实的情况没那么戏剧化,我认为,迁移之所以比预期的容易,可能是因为我在开始之前就已经建立了一个平台,或者说,是一个模块化架构。如果几年前就做出架构决策,事情可能会更容易,但往往由于任意原因被故意推迟了。

本系列不是 KMP 教程。相反,这是一个关于真实应用的案例研究,该应用已经有用户、历史以及沉淀在代码中的假设。第 1 部分涵盖基础。

起点

BikeHub 是一个自行车维护应用。它从 Strava 拉取你的骑行里程,提醒你更换链条、飞轮和刹车片,并在服务到期时通知你。它最初是为自己尝试新库和最佳实践而开发的,后来由于受众需求才发布给更广泛的受众。

在迁移开始时,它已经是一个纯 Android 应用,拆分为 24 个功能模块和 14 个核心模块(在迁移之前)。

迁移前做出的选择

Ktor Client 和 Koin 从第一天起就在项目中。两者都已经支持 Kotlin Multiplatform。我将网络和数据层迁移到了共享代码。当依赖注入代码不需要替换,只需要移动时,Ktor Client 是我更喜欢的选择,这工作量完全不同。

SQLDelight 是一个深思熟虑的选择。当我想添加本地持久化时,KMP 已经在地平线上,所以我选择了 SQLDelight 而不是 Room(特别是因为从一开始就是多平台迁移,而 Room 当时不提供多平台支持。现在有些支持了)。它存储自行车、组件、事件和一些更多数据。当迁移达到持久化层时,几乎没剩下什么工作了。Schema 和生成的代码已经是平台无关的,只有数据库平台部分需要切换,这要归功于它内存中的原生驱动,这让我在 Android 上避免了错误。

要点:如果跨平台甚至是一种可能性,选择 KMP 是兼容的,并且消除了重写整个应用程序的未来迁移痛苦。

BikeHub 是一个人的表演。它以小增量移动,经常有很长的停顿——作为一个 Android 开发者,维护和编写三个原生应用根本不现实。

所以我定了一条规则:Android 应用必须在每一步之后保持可发布状态。每次构建、测试和发布都必须自己通过,所以迁移可以在任何时候暂停,而不会留下半破坏的状态。

模块化做了繁重的工作

如果我必须命名一个单一的推动者,那不会是一个库。那是模块结构。

模块的代码只能在特定目标编译通过后,才能为该目标编译。这给你一个模块化的代码库,你可以逐个模块回答。在一个单体应用中,有一个整个应用的答案,而且通常“还没有”。

图表是这样工作的。它从底部开始,它在依赖顺序中强迫你。

实践

功能模块很早就变成了 KMP 库。

简单地把 Android Gradle 插件切换到 KMP 库。模块移动了代码到 commonMain,编译了,看看什么坏了。模块只构建到那个点,所以它的 Android-only 源代码级依赖像以前一样解析,每个失败都是源代码级的:主要是 Compose 导入,加上一些 Android API。所以没有抽象的迁移。每个功能模块使用 build.gradle.kts。

当我把 UI 重构为 KMP 时,第一步 0.1 抽象了 Android(和现在的 KMP)ViewModel,使整个 UI 逻辑可测试,并为 Jetpack Compose 构建。

核心模块一个接一个地重构,从领域模型、转换器和仓库实现开始。没有平台代码在那里,预期相当容易,通过交换 Android Library 插件与多平台之一。网络模块只是由于 Ktor Client 比 Android 多平台之一难一点。

我发现提取的模块越少,越容易,特别是从 commonMain 中给每个特定上游模块使用的。用于 iosMain 或 androidMain 中的依赖在 commonMain 中结束,但在迁移期间在我的脑海中更远。

只有那时才能构建功能模块的任何东西,但 Android 是第一遍显示我需要在内部修复什么的地方。

API 在一个小接口上走,平台特定代码被推到边缘。任何触及 Android 平台特定的实现都跨越层。

模块做了双重工作。它们定义了迁移的顺序,它们限制了爆炸半径:如果转换一个模块错误,只有那个模块受影响。

它们也做了暂停检查。我可以停止中途迁移,做一些功能开发,推出一两个版本,然后几周(月)后继续,知道确切的事情停留在哪里。模块图有效地是进度条。

故意做薄的 iOS 应用

iOS 应用模块是 304 行的 Swift。它包含应用入口点、托管共享 UI 的 ComposeUIViewController,以及连线通知和 Firebase。其他一切都在 KMP 中。

这是有意的。Swift 的每一行只造福一个平台,并且是平台特定的。当某些东西需要平台特定的实现时,它进入 KMP 的 iosMain source set。当它在 Android 和 Web 同伴旁边测试时,它使用相同的接口。

值得一提的是,大约 5,500 行的平台特定代码是 @Preview 函数。

最大的平台特定代码在 core-messaging 模块中,负责通知、本地联系人、深度链接、文件存储处理和深度链接。

否则,连接信息有一个平台特定的代码,其他的是平台抽象的。

我会做得不同的地方

从第一个转换的模块就添加 CI 的第二个目标。Android-only 模块与现有的 Android Library Gradle 插件一直工作得很好,直到另一个目标试图构建它。所以交换 Android Library Gradle 插件到 KMP Library,然后另一个平台立即显示隐藏的问题。拥有小的、单一责任的模块核心从一开始就会节省一些时间。

接下来

这是基础:一个出于其他原因变得比计划更便携的代码库。不是一切都顺利,但系列的其余部分是关于确切的部分:

  • 第 2 部分:什么免费移动了,什么受伤了。可移植架构几乎机械地移动,平台耦合不。
  • 第 3 部分:在不丢失安全网的情况下将 UI 分享出来。分享应用代码变成容易;生态系统围绕它。
  • 第 4 部分:运送 WebAssembly。在生产中在浏览器中获取一个多平台应用是另一回事。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注