Skip to content

frankois944/kmp-mvvm-exploration

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

207 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ KMP MVVM Exploration

Kotlin Swift Koin SKIE

A playground exploring the MVVM pattern with KMP ViewModel on iOS (SwiftUI & UIKit).

Important

This project is a Proof of Concept (PoC) and a collection of experiments. It is intended for exploration and learning, not as a strict production-ready guide.


πŸ“Œ Overview

Implementing MVVM in Kotlin Multiplatform for iOS can be tricky due to lifecycle management. While Android has first-class support for ViewModel, iOS requires workarounds to ensure proper memory management and lifecycle alignment.

This repository demonstrates various ways to bridge the gap between KMP and iOS.

πŸ§ͺ What's Inside?


πŸ’‘ Pro Tip: Optimize Your iOS Exports

Tip

Use explicitAPI in Gradle to control code visibility.

By default, all public Kotlin code is exported to the iOS framework. Reducing visibility (internal, private) leads to:

  • ⚑ Faster build times
  • πŸ“‰ Smaller binary size
  • πŸš€ Better Xcode autocomplete performance

πŸ“‹ Requirements

To properly use KMP ViewModels on iOS, we need to handle the export and lifecycle.

1. Export Dependencies

In your shared Gradle file, you must export the androidx.lifecycle.viewmodel so it's accessible in Swift.

kotlin {
    // ...
    cocoapods { // or framework
        export(libs.androidx.lifecycle.viewmodel)
    }
    
    sourceSets {
        commonMain.dependencies {
            api(libs.androidx.lifecycle.viewmodel)
        }
    }
}

2. Configure SKIE

SKIE is used to bridge Kotlin Flows to Swift effortlessly.

skie {
    features {
        enableSwiftUIObservingPreview = true // For >= iOS 15
        enableFutureCombineExtensionPreview = true
        enableFlowCombineConvertorPreview = true
    }
}

3. Handle Lifecycle (The SharedViewModel Wrapper)

We wrap the KMP ViewModel in an ObservableObject to align its lifecycle with SwiftUI views.

class SharedViewModel<VM: ViewModel>: ObservableObject {
    private let key = String(describing: type(of: VM.self))
    private let viewModelStore = ViewModelStore()

    init(_ viewModel: VM = .init()) {
        viewModelStore.put(key: key, viewModel: viewModel)
    }

    var instance: VM {
        (viewModelStore.get(key: key) as? VM)!
    }

    deinit {
        viewModelStore.clear() // Triggers onCleared() in Kotlin
    }
}

πŸ—οΈ The ViewModel Pattern

The MainScreenViewModel provides a common logic for both platforms.


πŸ“± SwiftUI Integration

SKIE Observable (iOS 15 and later)

Uses SKIE's SwiftUI observing which leverages the .task modifier. πŸ”— Example Code

SKIE Observable (iOS 14 and earlier)

A custom implementation using .onAppear for older iOS versions. πŸ”— Example Code

MVVM using Macro

Uses a custom Swift Macro to automatically wrap KMP ViewModels. πŸ”— Example Code

Pure SwiftUI MVVM

Standard SwiftUI ObservableObject using KMP services via Koin, without using KMP ViewModel. πŸ”— Example Code


πŸ›οΈ UIKit Support

UIKit is fully supported by combining SharedViewModel with SKIE Combine extensions. πŸ”— Example Code


πŸ’Ύ Database

Example implementation using Room KMP. πŸ”— Database Module

Important

Avoid exporting generated database code to iOS. Use a parent module to hide the database implementation details and only export necessary interfaces. Large schemas can significantly impact build times and binary size.


πŸ’‰ Dependency Injection

This project uses Koin with Annotations.

ViewModel Injection Fix

To use Koin ViewModels in a shared module without Compose, add this helper: πŸ”— KMPViewModelAnnotation.kt

Accessing Koin from Swift

  1. Export Koin Helpers: AppInit.ios.kt
  2. Setup Swift Context:
    // Store somewhere inside a singleton/static your Kotlin's KoinApplication instance for later usage
    AppContext.shared.koinApplication = koinApp // Your initialized Koin application
  3. Use Swift Helpers: KoinHelper.swift

Usage in Swift:

// Equivalent of 'get()' koin's methos
let accountService: AccountService = koinGet()

// Equivalent of 'by inject()' koin method
lazy var accountService: AccountService = koinGet() 

// With parameters
let logger: KermitLogger = koinGet(parameters: ["MainScreen"])

// ViewModel with parameters
@StateObject private var viewModel = StateObject(wrappedValue: SharedViewModel(koinGet(parameters: ["ID"])))

πŸ“ Logging & DataStore