KMP-ObservableViewModel

repository·master·Indexed 20 days ago

https://github.com/rickclephas/kmp-observableviewmodel

A library enabling AndroidX/Kotlin ViewModels to work with SwiftUI in Kotlin Multiplatform projects. It provides specialized property wrappers (@StateViewModel, @ObservedViewModel, @EnvironmentViewModel), lifecycle management, and support for the Apple Observation framework. The library includes kmp-observableviewmodel-core for Kotlin and KMPObservableViewModelSwiftUI for Swift, offering tools to handle child ViewModels and prevent JobCancellationException via the Cancellable interface.

Tokens
1.9K
Snippets
8
Records
8
Agent score
22%

What's inside KMP-ObservableViewModel

  1. Prevent JobCancellationException with Cancellable ViewModel

    master

    When subclassing a Kotlin ViewModel in Swift and using Combine to observe flows (especially via KMP-NativeCoroutines), the viewModelScope might be cancelled before the Swift object is deinitialized. This can cause Combine publishers to fail with a JobCancellationException.

    To fix this, make your Swift ViewModel conform to Cancellable and perform cleanup in the cancel() function. The library ensures cancel() is called before the ViewModel is cleared.

    import Combine
    import KMPNativeCoroutinesCombine
    import shared
    
    class TimeTravelViewModel: shared.TimeTravelViewModel, Cancellable {
        private var cancellables = Set<AnyCancellable>()
    
        override init() {
            super.init()
            createPublisher(for: currentTimeFlow)
                .assertNoFailure()
                .sink { time in print("It's \(time)") }
                .store(in: &cancellables)
        }
    
        func cancel() {
            cancellables = []
        }
    }
  2. Install KMP-ObservableViewModel in Kotlin

    master

    To use the library in your shared Kotlin module, add the kmp-observableviewmodel-core dependency and opt-in to kotlinx.cinterop.ExperimentalForeignApi in your build.gradle.kts file.

    kotlin {
        sourceSets {
            all {
                languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
            }
            commonMain {
                dependencies {
                    api("com.rickclephas.kmp:kmp-observableviewmodel-core:1.0.6")
                }
            }
        }
    }
  3. Enable Apple Observation framework support

    master

    To allow your Kotlin ViewModels to benefit from the modern Apple Observation framework (for more efficient re-rendering), add Observable conformance to the core ViewModel class in your KMPObservableViewModel.swift file.

    import Observation
    import shared // This should be your shared KMP module
    
    extension Kmp_observableviewmodel_coreViewModel: @retroactive Observable { }
  4. Configure Swift ViewModel Interop

    master

    To enable the library's functionality in Swift, create a KMPObservableViewModel.swift file and add a retroactive conformance to the core ViewModel class. This allows your Kotlin ViewModels to be used with SwiftUI property wrappers.

    import KMPObservableViewModelCore
    import shared // This should be your shared KMP module
    
    extension Kmp_observableviewmodel_coreViewModel: @retroactive ViewModel { }
  5. Handle Child ViewModels in Swift

    master

    If your Kotlin ViewModels expose child ViewModels (e.g., via a StateFlow), you must use specific patterns to prevent premature deallocation in Swift.

    1. In Kotlin, use the @NativeCoroutinesRefinedState annotation instead of @NativeCoroutinesState.
    2. In Swift, create an extension property using the childViewModel(at:) function.

    For collections (lists, sets, dictionaries) containing view models, use childViewModels(at:).

    // Kotlin side
    class MyParentViewModel: ViewModel() {
        @NativeCoroutinesRefinedState
        val myChildViewModel: StateFlow<MyChildViewModel?> = MutableStateFlow(null)
    }
    // Swift side
    extension MyParentViewModel {
        var myChildViewModel: MyChildViewModel? {
            childViewModel(at: \.__myChildViewModel)
        }
    }
  6. Use ViewModels in SwiftUI

    master

    Once configured, you can use Kotlin ViewModels in SwiftUI using specialized property wrappers that mirror standard ObservableObject patterns:

    ObservableObjectViewModel
    @StateObject@StateViewModel
    @ObservedObject@ObservedViewModel
    @EnvironmentObject@EnvironmentViewModel
    environmentObject(_:)environmentViewModel(_:)

    Example usage:

    import SwiftUI
    import KMPObservableViewModelSwiftUI
    import shared
    
    struct ContentView: View {
        @StateViewModel var viewModel = TimeTravelViewModel()
    }
  7. Install KMP-ObservableViewModel in Swift

    master

    You can add the library to your Swift project using Swift Package Manager (SPM) or CocoaPods.

    Swift Package Manager: Add https://github.com/rickclephas/KMP-ObservableViewModel.git to your Package.swift or via Xcode's 'Add Packages' menu.

    CocoaPods: Add the following to your Podfile:

    pod 'KMPObservableViewModelSwiftUI', git: 'https://github.com/rickclephas/KMP-ObservableViewModel.git', tag: 'v1.0.6'
    // SPM dependency example
    .package(url: "https://github.com/rickclephas/KMP-ObservableViewModel.git", from: "1.0.6")
  8. Create a ViewModel in Kotlin

    master

    Create your ViewModels by extending com.rickclephas.kmp.observableviewmodel.ViewModel. Note that this library uses a slightly different stateIn and MutableStateFlow constructor compared to standard AndroidX/Kotlin Coroutines to ensure state changes propagate correctly to SwiftUI.

    Key differences:

    • Use com.rickclephas.kmp.observableviewmodel.stateIn instead of kotlinx.coroutines.flow.stateIn.
    • Use com.rickclephas.kmp.observableviewmodel.MutableStateFlow which requires passing the viewModelScope in its constructor.
    • viewModelScope is a wrapper around the actual CoroutineScope, accessible via ViewModelScope.coroutineScope.
    import com.rickclephas.kmp.observableviewmodel.ViewModel
    import com.rickclephas.kmp.observableviewmodel.MutableStateFlow
    import com.rickclephas.kmp.observableviewmodel.stateIn
    
    open class TimeTravelViewModel: ViewModel() {
    
        private val clockTime = Clock.time
    
        val actualTime = clockTime.map { formatTime(it) }
            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "N/A")
    
        private val _travelEffect = MutableStateFlow<TravelEffect?>(viewModelScope, null)
        val travelEffect = _travelEffect.asStateFlow()
    }