Library / SDK
rickclephas/KMP-ObservableViewModel avatar
rickclephas/KMP-ObservableViewModel

KMP-ObservableViewModel: running a Kotlin ViewModel inside SwiftUI

Library to use AndroidX/Kotlin ViewModels with SwiftUI

712 stars32 forksSwiftMIT

At a glance

What is it?
KMP-ObservableViewModel is a Kotlin Multiplatform library that lets an AndroidX ViewModel drive SwiftUI, by swapping in a ViewModel base class and a MutableStateFlow that carries a scope so state changes cross the language boundary. It suits teams with a shared Kotlin core and an iOS front end, and it does nothing for a SwiftUI app with no Kotlin in it.
Who is it for?
KMP-ObservableViewModel is worth adopting when a project already has a shared Kotlin module holding its state and wants that same code to drive an iOS or macOS interface, because the whole cost of the library is a different ViewModel import, a different stateIn import and a MutableStateFlow that takes a scope, and because the Apple platform targets are supported out of the box.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 46 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Three import changes, and that is the migration

The library was previously known as KMM-ViewModel, and the description is narrow: it allows you to use AndroidX and Kotlin ViewModels with SwiftUI.

The migration cost is stated as tiny, and the documentation backs that up with a diff. The ViewModel superclass comes from the library instead of AndroidX:

diff
- import androidx.lifecycle.ViewModel
+ import com.rickclephas.kmp.observableviewmodel.ViewModel

open class TimeTravelViewModel: ViewModel() {

The second change is the stateIn import:

diff
- import kotlinx.coroutines.flow.stateIn
+ import com.rickclephas.kmp.observableviewmodel.stateIn

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

And the third is the MutableStateFlow constructor, which now takes a scope as its first argument:

diff
- import kotlinx.coroutines.flow.MutableStateFlow
+ import com.rickclephas.kmp.observableviewmodel.MutableStateFlow
 
-    private val _travelEffect = MutableStateFlow<TravelEffect?>(null)
+    private val _travelEffect = MutableStateFlow<TravelEffect?>(viewModelScope, null)

The reason for that last one is the point of the whole library. The documentation says these minor differences will make sure that state changes are propagated to SwiftUI. A StateFlow on the Kotlin side has to be able to keep collecting after the SwiftUI side has torn down and rebuilt, and a scope attached to the flow is what makes that lifecycle work across the boundary rather than at the call site.

The result is that a ViewModel written for Android is not much different from one written for a shared Kotlin module. The library's own example class is a plain open class extending ViewModel with a clock, a stateIn call and an asStateFlow call, and the documentation says the Android usage is just like any other AndroidX ViewModel.

A ViewModel that takes a scope, wrapped so you can reach the scope

The scope detail is worth spelling out, because it is the only non-obvious rule in the shared code.

A note in the documentation says viewModelScope is a wrapper around the actual CoroutineScope which can be accessed via the ViewModelScope.coroutineScope property. So the library's ViewModel does not expose a CoroutineScope directly. It exposes a wrapper, and the wrapper is what you pass to stateIn and to the library's MutableStateFlow constructor.

That is a deliberate difference from AndroidX, where viewModelScope is a property of type CoroutineScope and MutableStateFlow takes a single initial value. In the library, the scope travels with the flow, which is what lets the flow's collection be tied to the view model's lifetime rather than to whichever consumer happens to be attached at the time.

The shared module example shows the full shape. The ViewModel keeps a private MutableStateFlow with a TravelEffect type parameter, and exposes a read-only asStateFlow of it. The actualTime flow is built by mapping a clock time and then calling stateIn with viewModelScope, SharingStarted.WhileSubscribed() and a default value of "N/A".

That SharingStarted.WhileSubscribed() choice is worth noticing. It means the upstream clock collection only runs while there is a subscriber, which is the behaviour you want when the source is a timer, and it is the same choice AndroidX developers make, so the shared code does not have to change shape when it moves between platforms.

SwiftUI interop only exists on the Apple targets

The compatibility table is the first thing to read, and it makes a distinction that a lot of KMP libraries blur. The library can be used in any KMP project, but not all targets support SwiftUI interop.

Every target listed is supported, and every one of them supports AndroidX. The SwiftUI column is where the split happens. iOS, macOS, tvOS and watchOS all support SwiftUI interop. Android, JVM, linuxX64, linuxArm64, mingwX64, JS and Wasm do not.

That split is architectural rather than incidental. SwiftUI interop means the Kotlin side has to hand objects to Swift, and that means a cinterop bridge and a generated Objective-C layer, and neither of those exists outside the Apple platforms. The library still works on the other targets, and the same ViewModel class runs there, but the state is observed from Kotlin rather than from Swift.

The practical consequence for planning is that the shared module is genuinely shared across all eleven targets, and the Swift side is an Apple-only addition on top of it. A project that wants the same ViewModel driving an Android fragment and an iOS screen can have it. A project that wants the same ViewModel driving an Android fragment and a React web component cannot, and should not expect this library to help with that.

One artifact per Kotlin version, and the matrix is long

The versioning is unusual and worth understanding before you pick a version.

The library publishes separate artifacts for different Kotlin versions, with the compatibility table listing version, version suffix, Kotlin, Coroutines and AndroidX Lifecycle for each. The latest entry, marked as latest with no suffix, is Kotlin 2.4.20, Coroutines 1.10.1 and AndroidX Lifecycle 2.9.2. Below it, 1.0.6 and 1.0.5 sit on Kotlin 2.4.0 with Coroutines 1.10.1 and Lifecycle 2.8.7, 1.0.4 and 1.0.3 on 2.3.21 and 2.3.20, and so on back through the 1.0.0 and the BETA releases, which range from Kotlin 2.2.10 down to 2.0.0, with Coroutines moving from 1.10.1 to 1.8.1 in the older rows.

The consequence is that your Kotlin version picks your artifact, not the other way around. The dependency coordinate in the shared module carries the version explicitly:

kotlin
kotlin {
    sourceSets {
        all {
            languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
        }
        commonMain {
            dependencies {
                api("com.rickclephas.kmp:kmp-observableviewmodel-core:1.1.0")
            }
        }
    }
}

The opt-in to kotlinx.cinterop.ExperimentalForeignApi is required because the interop layer uses cinterop, and the documentation asks you to set it for all source sets.

The release history matches this scheme. Alongside v1.1.0 from 2026-09-08 there is a v1.0.6-kotlin-2.4.20-Beta2 build from 2026-07-30, which is a preview artifact for a Kotlin version, and the plain v1.0.6 from 2026-07-15. The suffix convention exists so that a pre-release of the library can be published against a Kotlin preview version.

Turning a StateFlow into a Swift property needs a second library

On the Swift side, the story is about turning a Kotlin StateFlow into something SwiftUI can observe.

The documentation recommends the @NativeCoroutinesState annotation from KMP-NativeCoroutines, a separate project by the same author, to turn your StateFlows into properties in Swift:

kotlin
@NativeCoroutinesState
val travelEffect = _travelEffect.asStateFlow()

An alternative is offered in a collapsed section, where you write the extension property yourself in the iOS or Apple source set:

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

The Swift side then needs the package added to Package.swift:

swift
dependencies: [
    .package(url: "https://github.com/rickclephas/KMP-ObservableViewModel.git", from: "1.1.0")
]

Or added in Xcode through File and Add Packages with the same URL. A CocoaPods route is documented as an alternative, with the pod named KMPObservableViewModelSwiftUI and a tag of v1.1.0.

The last step is the extension that makes the generated type conform to SwiftUI's ViewModel protocol:

swift
import KMPObservableViewModelCore
import shared // This should be your shared KMP module

extension Kmp_observableviewmodel_coreViewModel: @retroactive ViewModel { }

After that the documentation says you can use your view model almost as if it were an ObservableObject, with a table mapping the ObservableObject property wrappers to the ViewModel ones. The import of the shared module and the retroactive conformance on the generated class name are the two things that will not match your project, since the module name and the generated type name come from your own configuration.

Where KMP-ObservableViewModel is the wrong tool

The first limit is that it presumes a Kotlin core already exists. The library connects a Kotlin ViewModel to SwiftUI; it does not give a SwiftUI app a state layer. If your application is written in Swift and has no reason to share logic with an Android app, adding a Kotlin Multiplatform module, a Gradle build, a cinterop bridge and a generated Objective-C layer to observe one value is a large cost for a small gain.

The second limit is the extra dependency on KMP-NativeCoroutines for the recommended way of observing state in Swift. The documentation calls it a strong recommendation, and the alternative it offers is a hand-written extension property, which is one value at a time rather than a reactive stream. A project that wants real Swift-side reactivity is taking two libraries, and the second one has its own installation instructions to follow.

The third is the version matrix. The library needs a specific Kotlin, Coroutines and AndroidX Lifecycle combination, and there is a long table of published artifacts to choose from. When your Kotlin version moves, you are looking for the matching row, and the older BETA rows show what happens to the older end of the range over time. A team that upgrades Kotlin often will feel this more than one that settles.

The fourth is the ExperimentalForeignApi opt-in. It is a one-line build change, but it means the shared module is opted into an experimental interop API, and the underlying cinterop layer is exactly the part that is most likely to shift between Kotlin releases.

The fifth is the project size. The repository has a single author, MIT licensed, with the last push on 2026-09-08 and the latest release v1.1.0 from the same day. The sample/ directory holds an androidApp, an iosApp and a shared module, so there is a working reference, but a library at version 1.1.0 with an external companion project is a dependency whose maintenance you should check rather than assume.

Editorial conclusion

KMP-ObservableViewModel is worth adopting when a project already has a shared Kotlin module holding its state and wants that same code to drive an iOS or macOS interface, because the whole cost of the library is a different ViewModel import, a different stateIn import and a MutableStateFlow that takes a scope, and because the Apple platform targets are supported out of the box. It is the wrong choice for a SwiftUI-first project with no Kotlin core, and the wrong choice if you need the Swift interop on a non-Apple target. Before you adopt it, check the three things the compatibility table is explicit about: which targets support SwiftUI interop, since only iOS, macOS, tvOS and watchOS do while Android, JVM, the Linux targets, mingwX64, JS and Wasm do not; which Kotlin version you are on, since the latest build needs Kotlin 2.4.20 and every older Kotlin has its own published artifact; and that you opt in to ExperimentalForeignApi, which the interop layer requires.

Frequently asked questions

What does KMP-ObservableViewModel change in a Kotlin ViewModel?

Three things: the ViewModel superclass comes from com.rickclephas.kmp.observableviewmodel instead of androidx.lifecycle, stateIn comes from the same library's package instead of kotlinx.coroutines.flow, and MutableStateFlow is constructed with a viewModelScope as its first argument. Those differences are what make state changes propagate to SwiftUI.

Which KMP targets support SwiftUI interop in KMP-ObservableViewModel?

iOS, macOS, tvOS and watchOS. The library works on every listed target, and AndroidX is supported on all of them, but the SwiftUI column is marked only for the four Apple targets, so Android, JVM, the Linux targets, mingwX64, JS and Wasm have no SwiftUI interop.

How do I observe a Kotlin StateFlow from SwiftUI?

The documentation recommends the @NativeCoroutinesState annotation from the separate KMP-NativeCoroutines project, or you can write an extension property yourself in the Apple source set. On the Swift side you then extend the generated ViewModel class to conform to ViewModel with @retroactive, after adding the package to Package.swift or through Xcode.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. rickclephas/KMP-ObservableViewModel on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/rickclephas-kmp-observableviewmodel.svg)](https://hysenlabs.com/projects/rickclephas-kmp-observableviewmodel)

Was this useful?