从零到三端:用 Kotlin Multiplatform + Compose 构建跨平台财务追踪应用
让每一次增长,都有迹可循。
前言
作为一名 Kotlin 开发者,一直想尝试 Kotlin Multiplatform (KMP) + Compose Multiplatform 的全栈跨平台方案。最近完成了一个名为 Gainful(盈迹) 的个人财务追踪应用,支持 Android、iOS 和 Desktop 三端,今天分享一下整个开发过程中的思考和实践。
UI 设计 :使用 OpenDesign 辅助完成界面设计与原型验证。
为什么选择 KMP + Compose?
- 一套 Kotlin 代码,三端共享
- Compose 声明式 UI 开发效率高
- 原生性能,不像 WebView 方案有性能损耗
- 社区活跃,JetBrains 持续投入
效果展示
仪表盘

仪表盘页面展示当日盈亏、总盈亏、持仓走势图和关键指标,一目了然掌握投资概况。
持仓分析

持仓页面提供投资比重可视化(Treemap 图表)和持仓明细,支持实时行情显示。
交易记录

交易记录支持按类型筛选(全部/买入/卖出/股息),清晰展示每笔交易的详情。
设置

设置页面支持自定义涨跌颜色、交易时段配置、CSV 导入导出等功能。
技术栈选型
| 类别 | 技术 | 选择理由 |
|---|---|---|
| 网络请求 | Ktor | 官方 KMP 网络库,类型安全 |
| 本地存储 | Room | Google 官方跨平台 ORM |
| 依赖注入 | Koin | 轻量级,KMP 支持好 |
| 导航 | Navigation3 | Compose 官方导航方案 |
| UI 框架 | Compose Multiplatform | 声明式 UI,三端统一 |
架构设计
MVI 数据流
项目采用 MVI (Model-View-Intent) 架构,单向数据流驱动 UI 更新:
markdown
User Action → Intent → ViewModel → State → UI
↑ │
└────────────────────────────────────┘
这种架构的好处:
- 状态可预测,便于调试
- 单向数据流,逻辑清晰
- 易于测试和维护
Clean Architecture 分层
参考 Google 的 Now in Android 项目,采用 Clean Architecture 分层:
bash
shared/
├── commonMain/ # 平台无关代码
├── androidMain/ # Android 平台实现
├── iosMain/ # iOS 平台实现
└── jvmMain/ # Desktop 平台实现
core/
├── common/ # 通用工具
├── model/ # 数据模型
├── ui/ # 通用 UI 组件
├── data/ # Repository 接口
├── database/ # Room 数据库
├── domain/ # UseCase 业务逻辑
└── navigation/ # 导航配置
feature/
├── dashboard/ # 仪表盘
├── holdings/ # 持仓管理
├── transactions/ # 交易记录
└── settings/ # 设置
核心功能实现
1. 跨平台数据库(Room)
Room 在 KMP 中的使用体验非常好,几乎和 Android 原生一样:
kotlin
// 定义 Entity
@Entity(tableName = "transactions")
data class TransactionEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val stockCode: String,
val stockName: String,
val type: TransactionType,
val quantity: Int,
val price: Double,
val fee: Double,
val timestamp: Long
)
// 定义 DAO
@Dao
interface TransactionDao {
@Query("SELECT * FROM transactions ORDER BY timestamp DESC")
fun getAllTransactions(): Flow<List<TransactionEntity>>
@Insert
suspend fun insertTransaction(transaction: TransactionEntity)
}
注意点:Room 在 KMP 中需要使用 BundledSQLiteDriver,数据库文件会打包到应用中。
2. 跨平台网络请求(Ktor)
Ktor 是 KMP 的标准网络解决方案:
kotlin
// 定义 API 接口
interface StockApi {
suspend fun searchAssets(keyword: String): List<AssetDto>
suspend fun getStockPrice(code: String): StockPriceDto
}
// 实现
class StockApiImpl(private val client: HttpClient) : StockApi {
override suspend fun searchAssets(keyword: String): List<AssetDto> {
return client.get("api/assets/search") {
parameter("keyword", keyword)
}.body()
}
}
3. 跨平台 UI(Compose)
Compose Multiplatform 实现了真正的三端统一 UI:
kotlin
// 通用 Composable
@Composable
fun TransactionCard(transaction: Transaction) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = transaction.stockName,
style = MaterialTheme.typography.titleMedium
)
Text(
text = transaction.type.displayName,
color = if (transaction.type == TransactionType.BUY)
Color.Red else Color.Green
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(text = "${transaction.quantity}股 × ${transaction.price}")
}
}
}
4. 国际化(i18n)
使用 Compose Resources 实现多语言支持:
kotlin
// strings.xml (中文)
<string name="dashboard_title">仪表盘</string>
<string name="total_assets">总资产</string>
// strings-en.xml (英文)
<string name="dashboard_title">Dashboard</string>
<string name="total_assets">Total Assets</string>
// 使用
@Composable
fun DashboardScreen() {
Text(text = stringResource(Res.string.dashboard_title))
}
开发中的坑与解决方案
1. 日期时间处理
问题 :java.time.* 在 commonMain 中不可用。
解决 :使用 kotlinx-datetime:
kotlin
// 错误写法
// val now = java.time.LocalDateTime.now()
// 正确写法
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
2. iOS 框架配置
问题:iOS 需要静态框架,且需要配置 Team ID。
解决:
kotlin
// shared/build.gradle.kts
kotlin {
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "Shared"
isStatic = true
}
}
}
3. Compose 资源 ID 生成
问题:多模块资源 ID 冲突。
解决:每个模块独立命名空间:
kotlin
// 正确的导入方式
import gainful.feature.dashboard.generated.resources.Res
import gainful.feature.dashboard.generated.resources.dashboard_title
// 使用
stringResource(Res.string.dashboard_title)
开发体验总结
优点
- 开发效率高:一套代码三端运行,节省大量时间
- UI 一致性:Compose 保证了三端 UI 的一致性
- 类型安全:Kotlin 的类型系统让代码更健壮
- 热重载:Desktop 端支持 hot reload,开发体验好
需要注意
- 平台特定代码:仍需为各平台写少量平台特定代码
- 依赖版本:KMP 生态还在快速发展,依赖版本需要关注
- 调试工具:相比原生开发,调试工具还有提升空间
快速开始
Android
bash
./gradlew :androidApp:assembleDebug
Desktop
bash
./gradlew :desktopApp:run
iOS
- 打开
iosApp/iosApp.xcodeproj - 配置
TEAM_ID - 在 Xcode 中运行
项目地址
GitHub: github.com/Yoke0/Gainf...
总结
Kotlin Multiplatform + Compose Multiplatform 是一个非常有前途的跨平台方案。虽然目前还有一些坑需要踩,但整体开发体验已经非常不错。对于想要一套代码覆盖多端的开发者来说,这是一个值得尝试的技术栈。
如果你有任何问题,欢迎在评论区讨论!
标签:#Kotlin #KMP #ComposeMultiplatform #跨平台开发 #Android #iOS #Desktop