Skip to content
Ahmed Suliman edited this page Jul 28, 2026 · 2 revisions

Welcome to the KMPObservableBridge wiki!

KMPObservableBridge is a Swift-first, lifecycle-safe observation bridge for Kotlin Multiplatform ViewModels and SwiftUI.

It integrates with SKIE, KMP-NativeCoroutines, Combine publishers, callbacks, and custom asynchronous sources while preserving SwiftUI ownership, identity, lifetime, and dependency semantics.

Start here

Core principles

KMPObservableBridge is designed around these principles:

  • Kotlin remains the authoritative business-state owner.
  • SwiftUI retains its native ownership and identity semantics.
  • The bridge does not copy business state into a shadow Swift ViewModel.
  • Macro-selected fields use compile-time-checked Swift key paths.
  • No runtime reflection, Objective-C interception, or method swizzling is used.
  • Macro-configured wrappers observing the same model share a static observation hub.
  • Observation, cancellation, rebinding, and disposal follow deterministic lifetimes.
  • SKIE and KMP-NativeCoroutines integrations remain isolated package products.

Installation

Add the package through Xcode’s Package Dependencies interface:

https://github.com/sonmbol/KMPObservableBridge.git

Choose the product matching your integration:

Integration Product
SKIE StateFlow KMPObservableBridgeSKIE
KMP-NativeCoroutines NativeFlow KMPObservableBridgeNative
Combine, callbacks, or custom adapters KMPObservableBridge

SKIE setup

Import the generated Kotlin framework and the SKIE integration:

import shared
import KMPObservableBridgeSKIE

Enable synchronous current-value projection once in the application module:

extension SkieSwiftStateFlow:
    @retroactive KMPValueProperty {}

Declare the observable fields for an imported Kotlin ViewModel:

@KMPObservable(
    ArticleViewModel.self,
    fields: \.articleState
)
extension ArticleViewModel:
    @retroactive KMPStaticallyObservable {}

Swift macros cannot currently enumerate every member of an imported Kotlin type, so observable fields must be selected explicitly.

This preserves compile-time validation and avoids runtime reflection.

SwiftUI ownership

Own a Kotlin ViewModel for one SwiftUI identity:

struct ArticleScreen: View {
    @KMPStateObject
    private var article = ArticleViewModel()

    var body: some View {
        ArticleContent(viewModel: article)
    }
}

Observe a ViewModel owned elsewhere:

struct ArticleContent: View {
    @KMPObservedObject
    private var article: ArticleViewModel

    init(viewModel: ArticleViewModel) {
        _article = KMPObservedObject(viewModel)
    }

    var body: some View {
        Text(article.articleState.title)
    }
}

Propagate the same observation store through the SwiftUI environment:

ArticleContent()
    .kmpEnvironmentObject($article)

Consume it from a descendant:

struct ArticleContent: View {
    @KMPEnvironmentObject
    private var article: ArticleViewModel
}

Native Swift values and bindings

The wrapped property remains the original Kotlin ViewModel:

article.retry()
article.refresh()

The projected store provides synchronous Swift values from containers conforming to KMPValueProperty:

let message: String = $article.messageState
let loading: Bool = $article.loadingState

A genuinely writable Kotlin export produces a SwiftUI binding:

TextField(
    "Search",
    text: $article.searchText
)

Read-only StateFlows do not produce writable bindings. Update immutable Kotlin state through Kotlin actions.

Field-level dependencies

On iOS 17, macOS 14, tvOS 17, watchOS 10, and later, projected key-path-backed fields receive independent Observation dependencies.

A view reading:

$article.articleState

does not depend on an unrelated projected field.

Direct access through the wrapped Kotlin model intentionally registers a global model dependency:

article.articleState

On earlier supported systems, the bridge uses the correct global ObservableObject.objectWillChange fallback.

KMP-NativeCoroutines

Import the dedicated integration product:

import shared
import KMPObservableBridgeNative

Observe the exported NativeFlow explicitly:

struct NativeCoroutinesExampleView: View {
    @KMPStateObject(
        state: \.kmpObservationFlow,
        updatePolicy: .immediate
    )
    private var example = BridgeExampleViewModel()

    var body: some View {
        Text(example.nativeMessageValue)
    }
}

The NativeFlow supplies observation and cancellation. The separately exported Kotlin property supplies the current renderable value.

KMPObservableBridgeNative does not treat NativeFlow as a synchronous value container and does not expose SKIE symbols.

Lifecycle guarantees

Owned models

KMPStateObject owns its ViewModel for the lifetime of its SwiftUI identity.

An explicit disposer can be supplied:

@KMPStateObject(
    wrappedValue: ProfileViewModel(),
    dispose: { $0.clear() }
)
private var profile

A model conforming to KMPDisposable can be disposed automatically by an owning store.

Observed models

KMPObservedObject observes an externally owned model and never disposes it.

Rebinding

When an observed wrapper receives a different model:

  1. The previous observation is cancelled.
  2. The new model is stored.
  3. New observation begins.
  4. Existing dependencies are invalidated.
  5. Generation checks suppress stale emissions from the previous source.

Shared observation

Macro-configured wrappers observing the same Kotlin object share one static observation hub.

The final listener cancels the hub’s underlying observations.

Explicit adapters own the observation created for their wrapper.

Update policies

Coalesced delivery is the default:

@KMPStateObject
private var article = ArticleViewModel()

Same-turn changes are unioned and flushed once without losing independent field dependencies.

Event-sensitive consumers can request immediate delivery:

@KMPStateObject(updatePolicy: .immediate)
private var article = ArticleViewModel()

Examples and previews

The DailyPulse example demonstrates:

  • SKIE StateFlow observation
  • StateObject ownership
  • ObservedObject children
  • Environment propagation
  • Writable Kotlin bindings
  • KMP-NativeCoroutines
  • Callback cancellation
  • Combine publishers
  • Injector-owned ViewModels
  • Loading, error, empty, and populated states
  • Dark-mode previews

Preview presentation views use ordinary Swift values and do not initialize Koin, allocate Kotlin ViewModels, start coroutines, collect flows, or perform network requests.

Supported platforms

  • Swift 5.9 and later
  • iOS 15 and later
  • macOS 11 and later
  • tvOS 14 and later
  • watchOS 7 and later

Toolchain-specific manifests support matching SwiftSyntax releases for Swift 5.9 through Swift 6.2.

Open architecture discussions

We welcome alternative engineering proposals for two active investigations:

The existing implementation remains the tested baseline. Proposals should explain their impact on rendering, allocations, actor crossings, collectors, cancellation, source compatibility, and ARC/Kotlin GC lifetime.

Contributing

Contributions are welcome in areas including:

  • lifecycle and memory stress testing;
  • real-device performance measurements;
  • additional exporter integrations;
  • SKIE and NativeCoroutines fixtures;
  • SwiftUI hosting tests;
  • macro research;
  • documentation;
  • and API design proposals.

Please include measurable evidence for performance-sensitive changes.

Getting help


Built for Kotlin Multiplatform teams that want SwiftUI to remain SwiftUI.

Clone this wiki locally