跪拜 Guibai
← Back to the summary

Gainful: A Three-Platform Financial Tracker Built with Kotlin Multiplatform and Compose

From Zero to Three Platforms: Building a Cross-Platform Financial Tracking App with Kotlin Multiplatform + Compose

Make every gain traceable.

Foreword

As a Kotlin developer, I've always wanted to try the full-stack cross-platform solution of Kotlin Multiplatform (KMP) + Compose Multiplatform. I recently completed a personal financial tracking app called Gainful (盈迹), which supports Android, iOS, and Desktop. Today, I'd like to share the thinking and practice throughout the development process.

UI Design: OpenDesign was used to assist with interface design and prototype validation.

Why choose KMP + Compose?

Demo

Dashboard

dashboard.png

The dashboard page displays daily profit/loss, total profit/loss, portfolio trend charts, and key indicators, giving a clear overview of investment status at a glance.

Holdings Analysis

holdings.png

The holdings page provides investment weight visualization (Treemap chart) and holding details, supporting real-time market data display.

Transaction Records

records.png

Transaction records support filtering by type (All/Buy/Sell/Dividend), clearly displaying the details of each transaction.

Settings

settings.png

The settings page supports customizing rise/fall colors, trading session configuration, CSV import/export, and other features.

Tech Stack Selection

Category Technology Reason for Choice
Networking Ktor Official KMP networking library, type-safe
Local Storage Room Google's official cross-platform ORM
DI Koin Lightweight, good KMP support
Navigation Navigation3 Compose's official navigation solution
UI Framework Compose Multiplatform Declarative UI, unified across three platforms

Architecture Design

MVI Data Flow

The project adopts the MVI (Model-View-Intent) architecture, with unidirectional data flow driving UI updates:

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

Benefits of this architecture:

Clean Architecture Layering

Referencing Google's Now in Android project, Clean Architecture layering is adopted:

shared/
├── commonMain/        # Platform-independent code
├── androidMain/       # Android platform implementation
├── iosMain/           # iOS platform implementation
└── jvmMain/           # Desktop platform implementation

core/
├── common/            # Common utilities
├── model/             # Data models
├── ui/                # Common UI components
├── data/              # Repository interfaces
├── database/          # Room database
├── domain/            # UseCase business logic
└── navigation/        # Navigation configuration

feature/
├── dashboard/         # Dashboard
├── holdings/          # Holdings management
├── transactions/      # Transaction records
└── settings/          # Settings

Core Feature Implementation

1. Cross-Platform Database (Room)

The experience of using Room in KMP is excellent, almost identical to native Android:

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

// Define DAO
@Dao
interface TransactionDao {
    @Query("SELECT * FROM transactions ORDER BY timestamp DESC")
    fun getAllTransactions(): Flow<List<TransactionEntity>>
    
    @Insert
    suspend fun insertTransaction(transaction: TransactionEntity)
}

Note: Room in KMP requires the use of BundledSQLiteDriver, and the database file will be packaged into the application.

2. Cross-Platform Networking (Ktor)

Ktor is the standard networking solution for KMP:

// Define API interface
interface StockApi {
    suspend fun searchAssets(keyword: String): List<AssetDto>
    suspend fun getStockPrice(code: String): StockPriceDto
}

// Implementation
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. Cross-Platform UI (Compose)

Compose Multiplatform achieves a truly unified UI across three platforms:

// Common 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. Internationalization (i18n)

Multi-language support is implemented using Compose Resources:

// strings.xml (Chinese)
<string name="dashboard_title">仪表盘</string>
<string name="total_assets">总资产</string>

// strings-en.xml (English)
<string name="dashboard_title">Dashboard</string>
<string name="total_assets">Total Assets</string>

// Usage
@Composable
fun DashboardScreen() {
    Text(text = stringResource(Res.string.dashboard_title))
}

Pitfalls and Solutions During Development

1. Date and Time Handling

Problem: java.time.* is not available in commonMain.

Solution: Use kotlinx-datetime:

// Incorrect
// val now = java.time.LocalDateTime.now()

// Correct
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())

2. iOS Framework Configuration

Problem: iOS requires a static framework and Team ID configuration.

Solution:

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

3. Compose Resource ID Generation

Problem: Multi-module resource ID conflicts.

Solution: Independent namespaces for each module:

// Correct import method
import gainful.feature.dashboard.generated.resources.Res
import gainful.feature.dashboard.generated.resources.dashboard_title

// Usage
stringResource(Res.string.dashboard_title)

Development Experience Summary

Pros

  1. High Development Efficiency: One codebase runs on three platforms, saving significant time
  2. UI Consistency: Compose ensures UI consistency across three platforms
  3. Type Safety: Kotlin's type system makes code more robust
  4. Hot Reload: Desktop supports hot reload, providing a good development experience

Points to Note

  1. Platform-Specific Code: A small amount of platform-specific code is still required for each platform
  2. Dependency Versions: The KMP ecosystem is still developing rapidly, dependency versions need attention
  3. Debugging Tools: Compared to native development, debugging tools still have room for improvement

Quick Start

Android

./gradlew :androidApp:assembleDebug

Desktop

./gradlew :desktopApp:run

iOS

  1. Open iosApp/iosApp.xcodeproj
  2. Configure TEAM_ID
  3. Run in Xcode

Project Repository

GitHub: https://github.com/Yoke0/Gainful

Conclusion

Kotlin Multiplatform + Compose Multiplatform is a very promising cross-platform solution. Although there are still some pitfalls to navigate, the overall development experience is already quite good. For developers who want to cover multiple platforms with a single codebase, this is a tech stack worth trying.

If you have any questions, feel free to discuss in the comments!


Tags: #Kotlin #KMP #ComposeMultiplatform #CrossPlatformDevelopment #Android #iOS #Desktop

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

lotosbin同学

👍