/KMM-ViewModel

Library to share Kotlin ViewModels with SwiftUI

Primary LanguageKotlinMIT LicenseMIT

KMM-ViewModel

A library that allows you to share ViewModels between Android and iOS.

Compatibility

The latest version of the library uses Kotlin version 1.9.0.
Compatibility versions for older and/or preview Kotlin versions are also available:

Version Version suffix Kotlin Coroutines AndroidX Lifecycle
latest no suffix 1.9.0 1.7.2 2.6.1
1.0.0-ALPHA-10 no suffix 1.8.22 1.7.2 2.6.1
1.0.0-ALPHA-9 no suffix 1.8.21 1.7.1 2.5.1
1.0.0-ALPHA-8 no suffix 1.8.21 1.7.0 2.5.1
1.0.0-ALPHA-7 no suffix 1.8.21 1.6.4 2.5.1
1.0.0-ALPHA-6 no suffix 1.8.20 1.6.4 2.5.1
1.0.0-ALPHA-4 no suffix 1.8.10 1.6.4 2.5.1
1.0.0-ALPHA-3 no suffix 1.8.0 1.6.4 2.5.1

Kotlin

Add the library to your shared Kotlin module:

dependencies {
    api("com.rickclephas.kmm:kmm-viewmodel-core:1.0.0-ALPHA-11")
}

And create your ViewModels:

import com.rickclephas.kmm.viewmodel.KMMViewModel
import com.rickclephas.kmm.viewmodel.MutableStateFlow
import com.rickclephas.kmm.viewmodel.stateIn

open class TimeTravelViewModel: KMMViewModel() {

    private val clockTime = Clock.time

    /**
     * A [StateFlow] that emits the actual time.
     */
    val actualTime = clockTime.map { formatTime(it) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "N/A")

    private val _travelEffect = MutableStateFlow<TravelEffect?>(viewModelScope, null)
    /**
     * A [StateFlow] that emits the applied [TravelEffect].
     */
    val travelEffect = _travelEffect.asStateFlow()
}

As you might notice it isn't much different from an Android ViewModel.
The most obvious difference is the KMMViewModel superclass:

- import androidx.lifecycle.ViewModel
+ import com.rickclephas.kmm.viewmodel.KMMViewModel

- open class TimeTravelViewModel: ViewModel() {
+ open class TimeTravelViewModel: KMMViewModel() {

Besides that there are only 2 minor differences.
The first being a different import for stateIn:

- import kotlinx.coroutines.flow.stateIn
+ import com.rickclephas.kmm.viewmodel.stateIn

        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "N/A")

And the second being a different MutableStateFlow constructor:

- import kotlinx.coroutines.flow.MutableStateFlow
+ import com.rickclephas.kmm.viewmodel.MutableStateFlow

-    private val _travelEffect = MutableStateFlow<TravelEffect?>(null)
+    private val _travelEffect = MutableStateFlow<TravelEffect?>(viewModelScope, null)

These minor differences will make sure that any state changes are propagated to iOS.

Note: viewModelScope is a wrapper around the actual CoroutineScope which can be accessed via the ViewModelScope.coroutineScope property.

KMP-NativeCoroutines

I highly recommend you to use the @NativeCoroutinesState annotation from KMP-NativeCoroutines to turn your StateFlows into properties in Swift:

@NativeCoroutinesState
val travelEffect = _travelEffect.asStateFlow()

Checkout the KMP-NativeCoroutines README for more information and installation instructions.

Alternative

Alternatively you can create extension properties in your iOS source-set yourself:

val TimeTravelViewModel.travelEffectValue: TravelEffect?
    get() = travelEffect.value

Android

Use the view model like you would any other Android view model:

class TimeTravelFragment: Fragment(R.layout.fragment_time_travel) {
    private val viewModel: TimeTravelViewModel by viewModels()
}

Note: improved support for Jetpack Compose is coming soon.

Swift

Add the Swift package to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/rickclephas/KMM-ViewModel.git", from: "1.0.0-ALPHA-11")
]

Or add it in Xcode by going to File > Add Packages... and providing the URL: https://github.com/rickclephas/KMM-ViewModel.git.

CocoaPods

If you like you can also use CocoaPods instead of SPM:

pod 'KMMViewModelSwiftUI', '1.0.0-ALPHA-11'

Create a KMMViewModel.swift file with the following contents:

import KMMViewModelCore
import shared // This should be your shared KMM module

extension Kmm_viewmodel_coreKMMViewModel: KMMViewModel { }

After that you can use your view model almost as if it were an ObservableObject.
Just use the view model specific property wrappers and functions:

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

E.g. to use the TimeTravelViewModel as a StateObject:

import SwiftUI
import KMMViewModelSwiftUI
import shared // This should be your shared KMM module

struct ContentView: View {
    @StateViewModel var viewModel = TimeTravelViewModel()
}

It's also possible to subclass your view model in Swift:

import Combine
import shared // This should be your shared KMM module

class TimeTravelViewModel: shared.TimeTravelViewModel {
    @Published var isResetDisabled: Bool = false
}

Child view models

You'll need some additional logic if your KMMViewModels expose child view models.

First make sure to use the NativeCoroutinesRefinedState annotation instead of the NativeCoroutinesState annotation:

class MyParentViewModel: KMMViewModel() {
    @NativeCoroutinesRefinedState
    val myChildViewModel: StateFlow<MyChildViewModel?> = MutableStateFlow(null)
}

After that you should create a Swift extension property using the childViewModel(_, at:) function:

extension MyParentViewModel {
    var myChildViewModel: MyChildViewModel? {
        childViewModel(__myChildViewModel, at: \.__myChildViewModel)
    }
}

This will prevent your Swift view models from being deallocated too soon.

Note: for lists, sets and dictionaries containing view models there is childViewModels(_, at:).