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

从零到三端:用 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)

开发体验总结

优点

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

需要注意

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

快速开始

Android

bash 复制代码
./gradlew :androidApp:assembleDebug

Desktop

bash 复制代码
./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

相关推荐
gxgldyh1 小时前
Android Framework源码解析(六):Launcher3 启动流程详解——AMS 如何拉起系统桌面?
android
XiaoLeisj1 小时前
HandlerThread 与 IntentService 使用及源码解析
android·handler
我命由我1234511 小时前
Android Studio - Android Studio 自定义预览尺寸
android·java·ide·java-ee·android studio·android-studio·android runtime
取个名字太难了~13 小时前
越用越便宜,越用越强大:影像 SDK 的边际成本递减与网络效应实践
android·数码相机·美颜·相机连接·demu
AFinalStone17 小时前
Android 7系统网络(四)Native层(下)—netd Controller详解
android·网络
爱笑鱼17 小时前
Handler(一):post 之后,Runnable 到底在哪个线程执行?
android
木木子2218 小时前
[特殊字符] 音乐播放器——状态驱动的多媒体控制
android·开发语言·华为·php·harmonyos
雨白18 小时前
C 语言基础:结构体、联合体与枚举
android
阿巴斯甜19 小时前
Android 代码混淆
android