96SEO 2026-06-07 07:56 13
Zui近把自己的 NBA 数据应用 HoopsNow 从纯 Android 多模块架构迁移到了 KMP + CMP,实现了 Android/iOS 共享一套代码。
这篇文章记录整个迁移过程中的思路、踩坑和Zui终方案。

HoopsNow 是一个 NBA 数据展示应用,功Neng包括比赛比分、球队信息、球员搜索和收藏管理。
迁移前的架构参考了 Google 的 Now in Android 项目,是一个标准的 Android 多模块架构:
hoopsnow/
├── app/ # 入口 + Navigation3
├── core/ # 7 个核心模块
│ ├── common/ # 工具类
│ ├── data/ # Repository
│ ├── database/ # Room
│ ├── datastore/ # DataStore
│ ├── designsystem/ # 主题
│ ├── model/ # 数据模型
│ ├── network/ # Ktor
│ ├── testing/ # 测试工具
│ └── ui/ # 共享 UI
├── feature/ # 4 个功Neng模块
│ ├── games/
│ ├── teams/
│ ├── players/
│ └── favorites/
└── build-logic/ # 5 个 Convention Plugins
技术栈:Hilt + Navigation3 + Room + ViewModel + Coil
这套架构在纯 Android 场景下hen好用,模块边界清晰,构建并行度高。
但当我想把应用 到 iOS 时这些 Android 专属的库就成了障碍。
考虑过几个方案:
Zui终选了 KMP + CMP,原因hen简单:现有代码是 Kotlin + Compose,迁移成本Zui低,UI 也Neng共享。
一、创建 KMP 共享模块第一步是创建 KMP 共享模块。
// shared/build.gradle.kts
plugins {
alias
alias
alias
alias
alias
alias
}
kotlin {
androidTarget {
compilerOptions { jvmTarget.set }
}
listOf, iosArm64, iosSimulatorArm64).forEach {
it.binaries.framework {
baseName = "Shared"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation
implementation
implementation
implementation
// Ktor, SQLDelight, Koin, Voyager, Coil ...
}
androidMain.dependencies {
implementation
implementation
}
iosMain.dependencies {
implementation
implementation
}
}
}
二、数据库迁移:Room → SQLDelight
这是迁移中工作量Zui大的部分。
Room 不支持 KMP,必须换成 SQLDelight。
SQLDelight 用 .sq 文件定义表结构和查询,放在 commonMain/sqldelight/ 目录下:
-- Team.sq
CREATE TABLE TeamEntity (
id INTEGER PRIMARY KEY NOT NULL,
conference TEXT NOT NULL,
division TEXT NOT NULL,
city TEXT NOT NULL,
name TEXT NOT NULL,
fullName TEXT NOT NULL,
abbreviation TEXT NOT NULL
);
getAll: SELECT * FROM TeamEntity;
getById: SELECT * FROM TeamEntity WHERE id = ?;
upsert: INSERT OR REPLACE INTO TeamEntity VALUES ;
踩坑:SQLDelight 属性名
生成的 Queries 属性名基于 .sq 文件名,不是表名。
比如 Game.sq 生成 database.gameQueries,不是 database.gameEntityQueries。
Hilt 依赖 Android 的注解处理器,不支持 KMP。
Koin 是纯 Kotlin 实现,天然跨平台。
// commonMain - KoinModules.kt
val sharedModule = module {
// Network
single { KtorNbaNetwork) }
// Database
single { get.createDriver }
single { NbaDatabase) }
// Repositories
single { OfflineFirstGamesRepository, get) }
single { OfflineFirstTeamsRepository, get) }
single { OfflineFirstPlayersRepository, get) }
single { OfflineFirstFavoritesRepository, get) }
// ScreenModels
factory { GamesListScreenModel) }
factory { params -> GameDetailScreenModel, get) }
}
// 平台模块通过 expect/actual 提供
expect fun platformModule: Module
四、导航:Navigation3 → Voyager
Voyager 提供了 TabNavigator+Navigator)的组合,hen适合底部 Tab + 页面栈的场景。
@Composable
fun HoopsNowApp {
HoopsNowTheme {
TabNavigator {
Scaffold(
bottomBar = {
NavigationBar {
TabNavigationItem
TabNavigationItem
TabNavigationItem
TabNavigationItem
}
},
) {
CurrentTab
}
}
}
}
每个 Tab 内嵌独立的 Navigator,Tab 切换时各自的导航栈互不影响。
// 定义tab
object GamesTab : Tab {
override val options @Composable get = TabOptions(
index = 0u,
title = "Games",
icon = rememberVectorPainter,
)
@Composable
override fun Content {
Navigator) { navigator ->
SlideTransition
}
}
}
页面间传参hen简单直接:
// 传参class GameDetailScreen : Screen {// 使用navigator.push)}
五、状态管理:ViewModel → ScreenModel
Voyager 的 ScreenModel 和 ViewModel 几乎一模一样:
// 迁移前@HiltViewModelclass GamesListViewModel @Inject constructor(
private val gamesRepository: GamesRepository,) : ViewModel
{
val uiState = gamesRepository.getGames
.stateIn, Loading)
// 迁移后class GamesListScreenModel(
private val gamesRepository: GamesRepository,) : ScreenModel
{
val uiState = gamesRepository.getGames
.stateIn, Loading)
改动点:
@HiltViewModle+@Inject删掉换成Koin factory{}声明代码量反而少了
Koin 用parametersOf)传入参数
带参数的 ScreenModel 需要用 factory{params->}定义,使用时通过 koinScreenModel{parametersOf}传入。
// 定义factory { params -> GameDetailScreenModel, get) }// 使用val screenModel=koinScreenModel{parametersOf}
七、iOS 接入
iOS 端geng简单,只需要一个 SwiftUI壳: SwiftUI 通过 ComposeUIViewController嵌入 Compose UI。
// iOSApp.swift@mainstruct iOSApp: App
{
init
{
KoinHelperKt.doInitKoin
}
var body: some Scene
{
WindowGroup
{ ContentView }
}
}
// ContentView.swiftstruct ContentView: View
{
var body: some View
{
ComposeView
.ignoresSafeArea
}
struct ComposeView: UIViewControllerRepresentable
{
func makeUIViewController -> UIViewController
{
Main_iosKt.MainViewController
func updateUIViewController {}
}
shared 模块中提供 iOS 入口:
// iosMain - Main_ios.ktfun MainViewController=ComposeUIViewController{HoopsNowApp}
就这样,iOS 端就Neng跑起来了。
八、清理旧代码当前仓库为了对照仍保留部分corefeature历史代码不在 settings.gradle.kts 中参与构建。构建维度Yi经是 app+shared个模块。
整个迁移花了大约一天时间,其中数据库迁移和导航迁移占了大部分工作量。网络层和序列化本身就是 KMP 库基本不用改。
Ru果你的 Android 项目Yi经在用 Kotlin+Compose迁移到KMP+CMP成本比想象中低hen多。Zui大的障碍是 Room 和 Hilt 这两个专属库的替换但 SQLDelight 和Koindou是成熟的替代方案。
Zui大的收益是 iOS 端几乎零成本接入—只需要两个 Swift 文件就Neng跑起完整的应用。 说实话真香!
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback