从零到三端:用 Kotlin Multiplatform + Compose 构建跨平台财务追踪应用

11 阅读4分钟

从零到三端:用 Kotlin Multiplatform + Compose 构建跨平台财务追踪应用

让每一次增长,都有迹可循。

前言

作为一名 Kotlin 开发者,一直想尝试 Kotlin Multiplatform (KMP) + Compose Multiplatform 的全栈跨平台方案。最近完成了一个名为 Gainful(盈迹) 的个人财务追踪应用,支持 Android、iOS 和 Desktop 三端,今天分享一下整个开发过程中的思考和实践。

UI 设计:使用 OpenDesign 辅助完成界面设计与原型验证。

为什么选择 KMP + Compose?

  • 一套 Kotlin 代码,三端共享
  • Compose 声明式 UI 开发效率高
  • 原生性能,不像 WebView 方案有性能损耗
  • 社区活跃,JetBrains 持续投入

效果展示

仪表盘

dashboard.png

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

持仓分析

holdings.png

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

交易记录

records.png

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

设置

settings.png

设置页面支持自定义涨跌颜色、交易时段配置、CSV 导入导出等功能。

技术栈选型

类别技术选择理由
网络请求Ktor官方 KMP 网络库,类型安全
本地存储RoomGoogle 官方跨平台 ORM
依赖注入Koin轻量级,KMP 支持好
导航Navigation3Compose 官方导航方案
UI 框架Compose Multiplatform声明式 UI,三端统一

架构设计

MVI 数据流

项目采用 MVI (Model-View-Intent) 架构,单向数据流驱动 UI 更新:

User Action → Intent → ViewModel → State → UI
     ↑                                    │
     └────────────────────────────────────┘

这种架构的好处:

  • 状态可预测,便于调试
  • 单向数据流,逻辑清晰
  • 易于测试和维护

Clean Architecture 分层

参考 Google 的 Now in Android 项目,采用 Clean Architecture 分层:

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 原生一样:

// 定义 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 的标准网络解决方案:

// 定义 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:

// 通用 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 实现多语言支持:

// 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

// 错误写法
// val now = java.time.LocalDateTime.now()

// 正确写法
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())

2. iOS 框架配置

问题:iOS 需要静态框架,且需要配置 Team ID。

解决

// shared/build.gradle.kts
kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "Shared"
            isStatic = true
        }
    }
}

3. Compose 资源 ID 生成

问题:多模块资源 ID 冲突。

解决:每个模块独立命名空间:

// 正确的导入方式
import gainful.feature.dashboard.generated.resources.Res
import gainful.feature.dashboard.generated.resources.dashboard_title

// 使用
stringResource(Res.string.dashboard_title)

开发体验总结

优点

  1. 开发效率高:一套代码三端运行,节省大量时间
  2. UI 一致性:Compose 保证了三端 UI 的一致性
  3. 类型安全:Kotlin 的类型系统让代码更健壮
  4. 热重载:Desktop 端支持 hot reload,开发体验好

需要注意

  1. 平台特定代码:仍需为各平台写少量平台特定代码
  2. 依赖版本:KMP 生态还在快速发展,依赖版本需要关注
  3. 调试工具:相比原生开发,调试工具还有提升空间

快速开始

Android

./gradlew :androidApp:assembleDebug

Desktop

./gradlew :desktopApp:run

iOS

  1. 打开 iosApp/iosApp.xcodeproj
  2. 配置 TEAM_ID
  3. 在 Xcode 中运行

项目地址

GitHub: github.com/Yoke0/Gainf…

总结

Kotlin Multiplatform + Compose Multiplatform 是一个非常有前途的跨平台方案。虽然目前还有一些坑需要踩,但整体开发体验已经非常不错。对于想要一套代码覆盖多端的开发者来说,这是一个值得尝试的技术栈。

如果你有任何问题,欢迎在评论区讨论!


标签:#Kotlin #KMP #ComposeMultiplatform #跨平台开发 #Android #iOS #Desktop