If you're reading this, you probably want to know what's happening behind the scenes. KRelay isn't magic—it's just a carefully synchronized bridge between your shared Kotlin code and the messy world of platform lifecycles.
We built KRelay around three core problems we kept seeing in KMP projects:
The Pain: You call a Toast from a background thread in your ViewModel. Android crashes because you touched the UI from the wrong thread. iOS just ignores you.
Our Fix: Every dispatch in KRelay is automatically routed to the Main Thread (Looper on Android, Main Queue on iOS). You don't have to worry about withContext(Dispatchers.Main) anymore.
The Pain: You pass an Activity to a ViewModel. The user rotates the screen. The old Activity is destroyed, but the ViewModel still holds a reference to it. Memory Leak.
Our Fix: KRelay only stores WeakReferences to your platform implementations. When your Activity or UIViewController dies, the reference becomes null automatically. No onDestroy cleanup required.
The Pain: You navigate to a new screen. While it's loading, you trigger a "Success" toast. But the new Activity hasn't registered its listener yet. The event is lost. Our Fix: If no implementation is registered, KRelay pushes your action into a Buffered Queue. As soon as a listener registers, it "replays" the missed actions. No more lost signals during cold starts or rotations.
- Shared Code: Calls
KRelay.dispatch<ToastFeature> { ... }from any thread. - KRelay: Grabs a lock, finds the WeakRef for
ToastFeature. - Execution: Since the implementation is there, it immediately wraps your block in a
runOnMaincall. - Platform: The code runs safely on the UI thread.
- Shared Code: Dispatches an event while the UI is rotating.
- KRelay: Finds no registered implementation.
- Queueing: It wraps your action and puts it in the
pendingQueue. - Registration: 300ms later, the new Activity calls
KRelay.register(). - Replay: KRelay sees the pending queue, clears it, and runs every action on the Main Thread against the new Activity.
We keep the core as thin as possible, delegating the "heavy lifting" to platform-native tools:
- Android: We use a
Handler(Looper.getMainLooper())for dispatching andjava.lang.ref.WeakReference. - iOS: We use
dispatch_async(dispatch_get_main_queue())and Kotlin/Native's ownWeakReference.
We chose object KRelay because for 90% of apps, a global bridge is exactly what you want. It's zero-config and matches platform patterns like Dispatchers.Main.
Update (v2.0): We added the Instance API for Super Apps where you need isolated bridges per module.
We wanted KRelay.dispatch<MyFeature>. It’s type-safe, provides great IDE autocomplete, and prevents string-key typos. The trade-off is that it’s not directly callable from Swift, so we provide a tiny Swift-friendly wrapper.
KRelay is extremely lightweight.
- Memory: A typical app with 10 features and a few queued actions uses less than 1KB of RAM.
- CPU: A dispatch takes roughly 2µs. You could trigger thousands of these without breaking a sweat (though your UI might not appreciate it!).
KRelay stores its queue in memory. If the OS kills your app process, the queue is gone. This is by design. Events like Toasts and Navigation are ephemeral. For critical data that must survive process death (like a payment), use WorkManager or Room. KRelay is the messenger, not the database.
| Feature | KRelay | expect/actual | StateFlow | WorkManager |
|---|---|---|---|---|
| Platform Calls | ✅ Perfect | ✅ Good | ❌ No | ❌ No |
| Return Values | ❌ No | ✅ Yes | ✅ Yes | |
| State Management | ❌ No | ❌ No | ✅ Perfect | ❌ No |
| Guaranteed Execution | ❌ No | ✅ Yes | ✅ Yes | ✅ Yes |
| Process Death Survival | ❌ No | ✅ Yes | ✅ Yes |
Last Updated: April 7, 2026.