DEV Community

Cover image for Mastering Native Interop in KMP: Why KRelay is the “Ultimate Bridge” Multiplatform
Viet, Nguyen Tuan
Viet, Nguyen Tuan

Posted on

Mastering Native Interop in KMP: Why KRelay is the “Ultimate Bridge” Multiplatform

KRelay: The Native Interop Bridge for Kotlin Multiplatform

KMP isn't just about sharing code; it's about managing Native UI safely. KRelay is the tool that makes it possible.


In my 15 years of mobile engineering — from J2ME to Flutter and now Kotlin Multiplatform (KMP) — one "final boss" always remains: Native Interop. How do you trigger native UI components like Toasts, Camera, or Navigation from shared code without causing memory leaks, threading crashes, or losing events during screen rotation?

Enter KRelay — a library designed with the Unix Philosophy: "Do one thing and do it well."


The Core Value: Why KRelay?

Traditionally, calling platform-specific code from shared ViewModels requires complex patterns:

  • Boilerplate Overload: You often need MutableSharedFlow + emit + collectAsState + LaunchedEffect just for a simple notification.
  • Memory Leaks: Strong references to an Activity or ViewController in shared code prevent garbage collection, leading to OutOfMemoryError.
  • Event Loss: If you dispatch a command while the UI is recreating (e.g., during rotation), that event is often lost forever.

KRelay solves these issues through three fundamental pillars:

1. Weak Registry

KRelay automatically manages WeakReferences to your platform implementations. When a ViewController or Activity is destroyed, the reference is cleared automatically.

2. Sticky Queue

If your ViewModel dispatches an action while the UI is not ready (e.g., during a cold start or rotation), KRelay queues the action and replays it as soon as the UI registers.

3. Safe Dispatch

All commands are automatically executed on the platform's Main/UI thread (using Looper on Android and GCD on iOS), preventing threading-related crashes.


The "Ultimate Combo": KRelay + Adapter Pattern

One of the biggest hurdles in 2026 is that many modern Swift libraries (like Lottie or Alamofire) are "Pure Swift" and are not visible to Kotlin's cinterop.

The Adapter Pattern combined with KRelay is the most powerful solution:

  • The Strategy: Define an interface in commonMain (e.g., NavFeature). Create a Swift Adapter that implements this interface and calls your 3rd-party Swift library.
  • The Benefit: Your ViewModel stays pure and testable. KRelay acts as the safe messenger, handling the threading and lifecycle complexities of the native implementation.

Technical Deep-Dive

KRelay is a high-performance Runtime bridge, avoiding the complexity and build-time overhead of KSP or reflection.

Installation

Add the library to your shared module's dependencies:

// shared/build.gradle.kts
commonMain.dependencies {
    implementation("dev.brewkits:krelay:1.0.0")
}
Enter fullscreen mode Exit fullscreen mode

Core API Usage

Defining and using a feature is straightforward:

// 1. Define (commonMain)
interface ToastFeature : RelayFeature {
    fun show(message: String)
}

// 2. Dispatch (Shared Logic)
KRelay.dispatch<ToastFeature> { it.show("Hello!") }

// 3. Register (Android/iOS Native)
KRelay.register<ToastFeature>(AndroidToastImpl(context))
Enter fullscreen mode Exit fullscreen mode

Advanced Features

  • Priority System: Use ActionPriority (LOW to CRITICAL) to ensure important events like error dialogs are replayed first.
  • Performance Metrics: Monitor dispatch counts and queue statistics per feature to optimize your app.
  • Safe Clean-up: Use clearQueue<T>() in your ViewModel's onCleared() to prevent lambda capture leaks.

Project Architecture

KRelay's internal structure is optimized for platform-specific efficiency:

KRelay/
├── commonMain/        # Core registry and queue logic
│   └── KRelay.kt, Priority.kt, Metrics.kt
├── androidMain/       # Android Looper and ReentrantLock
│   └── Lock.android.kt, MainThreadExecutor.android.kt
└── iosMain/           # iOS GCD and NSRecursiveLock
    └── Lock.ios.kt, MainThreadExecutor.ios.kt, KRelay+Extensions.swift
Enter fullscreen mode Exit fullscreen mode

Important Limitations

  • Process Death: KRelay's queue is in-memory and does not survive process death. Never use it for critical transactions like payments.
  • Singleton Scope: While simple for most apps, large Super Apps should use Feature Namespacing to avoid registry conflicts.

Verdict

KRelay isn't trying to be a state manager or a background worker. It is a focused, reliable messenger for one-way UI commands. By combining KRelay with the Adapter Pattern, you gain the flexibility to use any native library while maintaining the cleanest shared code architecture possible.

Start building safer KMP apps today.

GitHub Repository: github.com/brewkits/krelay


Tags: #KotlinMultiplatform #KMP #NativeInterop #AndroidDev #iOSDev #CleanArchitecture

Top comments (2)

Collapse
 
tuan_nguyen_320fd3c5 profile image
Hung, Nguyen

Is KRelay only useful for Android? Since iOS doesn't destroy ViewControllers on screen rotation like Android Activities, do I really need an interoperability bridge there?

Collapse
 
tuan_nguyen_320fd3c5 profile image
Hung, Nguyen

The short answer is YES. In fact, KRelay might be even more critical for iOS stability than it is for Android.
While it is true that iOS does not suffer from Android's aggressive "destroy-and-recreate" lifecycle during configuration changes (like screen rotation), iOS introduces a different set of fatal problems when working with Kotlin Multiplatform (KMP).

Here is why KRelay is a "lifesaver" for iOS development in KMP:

  1. The ARC Memory Leak Trap (Retain Cycles) Unlike Android's Garbage Collector (GC), which can handle circular references effectively, iOS uses ARC (Automatic Reference Counting).

The Problem: If your Shared Code (Kotlin) holds a strong reference to a Swift implementation, and that Swift class holds a reference to a UIViewController, you create a Retain Cycle. The ViewController will never be deallocated, causing severe memory leaks.

KRelay's Solution: KRelay explicitly implements NativeWeakReference for iOS. When you register a feature, KRelay only holds a weak reference. As soon as the ViewController is popped, ARC cleans it up, and KRelay automatically detects that the reference is gone—preventing leaks entirely.

  1. The "Background Thread" Crash Kotlin Coroutines in shared code often run on Dispatchers.Default or Dispatchers.IO (background threads).

The Problem: iOS is extremely strict about UI updates. If your shared code calls a callback that touches UIKit or SwiftUI from a background thread, the app will crash immediately or trigger the Main Thread Checker.

KRelay's Solution: The iOS implementation of KRelay uses GCD (dispatch_async(dispatch_get_main_queue())) to guarantee that every single dispatched command is executed on the Main Thread, regardless of where it was called from.

  1. Handling "Dead Views" during Navigation While iOS doesn't destroy views on rotation, it definitely destroys them on navigation (e.g., popping a ViewController from the stack).

The Problem: Imagine a user starts a long task and immediately hits "Back". When the task finishes, the shared code tries to call a method on a now-deallocated Swift object. Without protection, this can lead to crashes or undefined behavior.

KRelay's Solution: Thanks to its registry system, KRelay checks if the implementation is still alive before dispatching. If the view is dead, it safely drops the event (or queues it if you re-register later), preventing crashes on dead objects.

  1. Concurrency Safety Synchronizing shared state between Kotlin Native and Swift is complex.

The Problem: Rapidly dispatching events from multiple threads can lead to race conditions or deadlocks on iOS.

KRelay's Solution: It implements a custom Lock using NSRecursiveLock specifically for iOS. This ensures that even under heavy load, your UI dispatch remains thread-safe and deadlock-free.

  1. Swift-Friendly API Kotlin's reified inline functions (generics) are lost when compiled to Objective-C headers for iOS.

The Solution: KRelay provides a Swift extension (KRelay+Extensions.swift) that wraps the raw Kotlin calls, allowing iOS developers to write clean, idiomatic Swift code like KRelay.shared.register(impl) instead of dealing with raw KClass lookups.

Conclusion: While Android needs KRelay primarily for Lifecycle/Rotation, iOS needs KRelay primarily for Memory Safety (ARC) and Thread Safety. It acts as a unified shield against the specific platform risks of both worlds.