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+LaunchedEffectjust 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")
}
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))
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'sonCleared()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
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)
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?
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:
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.
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.
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.
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.
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.