
Lightweight, leak‑free bridge delivering one‑way UI commands from shared ViewModels to native implementations; automatic weak references, sticky queued actions, main‑thread execution, easy testing.
Dispatch UI commands (Toast, Navigation, Permissions) from shared ViewModels to Android, iOS, Desktop JVM, and Web (WasmJs) — leak-free, rotation-safe, always on the Main Thread.
Most mobile apps treat everything (Toasts, Navigation, Alerts) as State. This is why you see "Ghost Toasts" popping up after rotation, or users stuck because a navigation event fired in the 300ms "blind spot" during an Activity restart.
| Approach | The Pain Point |
|---|---|
Pass Activity / UIViewController
|
Memory leaks and onDestroy boilerplate |
SharedFlow(replay=0) |
Events are lost during screen rotation |
StateFlow as Event |
Double-execution / Side-effects that "stick" |
Channels |
Single-observer only (unsuitable for UI + Analytics) |
KRelay is a Buffered Multicasting bridge. Your shared ViewModel signals an intent, and the platform fulfills it — exactly once, always on the Main Thread, even if the UI wasn't ready when you called it.
"State is for seeing, Event is for running."
KRelay is designed for mission-critical systems (VoIP, Fintech, SOS) where event delivery is non-negotiable.
Read the State vs. Event: Why your MVI/Redux app is probably leaking side-effects.
StateFlow for state, Channel for single-shot UI events) are completely sufficient and introduce zero external dependencies.| Scenario | Better Alternative | Why |
|---|---|---|
| Standard CRUD UI Events |
Channel<UiEvent> / SharedFlow
|
Simpler, standard Kotlin idioms, zero dependencies |
| Need a return value |
suspend fun + expect/actual
|
KRelay is fire-and-forget; cannot return results |
| Reactive UI state |
StateFlow / MutableStateFlow
|
State is for seeing; events are for running |
| Critical side-effects (payment, upload) |
WorkManager / iOS Background Tasks |
KRelay queue is memory-based; lost on process death |
| Database / Network | Room / SQLDelight / Ktor | Core business architecture belongs in Repositories |
When shared business logic must trigger platform APIs tightly coupled to the active Activity or UIViewController lifecycle:
Google Play Core / Apple StoreKit), Native Biometric Prompts (AndroidX Biometric / iOS LocalAuthentication), Photo Picker intents, or Push notification system bridges.expect/actual interfaces through 4–5 architectural layers (ViewModel → UseCase → Repository → Activity) creates fragile boilerplate and lifecycle leaks. With KRelay, the platform Activity or Composable dynamically registers when resumed and unregisters when disposed. KRelay buffers commands in its sticky queue during rotations or screen transitions, dispatching them the moment the native UI is ready.In large, enterprise-scale codebases where independent feature modules run under a single host shell:
Ride) and Team B (Food) running inside the same Super App (e.g., Grab, Uber, Gojek). Modules are strictly forbidden from having compile-time dependencies on each other.KRelay.create("Rides") vs. KRelay.create("Food")) with ScopeToken lifecycle guards. Feature teams dispatch cross-boundary intents dynamically without direct compile-time coupling or global event bus collision.KRelay now provides a Bill of Materials (BOM) to automatically align versions across all artifacts.
// shared/build.gradle.kts
sourceSets {
commonMain.dependencies {
// 1. (Recommended) Import the BOM
api(platform("dev.brewkits:krelay-bom:2.2.0"))
// 2. Add dependencies without specifying versions
implementation("dev.brewkits:krelay")
implementation("dev.brewkits:krelay-compose") // Optional: Compose helpers
implementation("dev.brewkits:krelay-flow") // Optional: Flow operators (v2.2.0+)
}
commonTest.dependencies {
implementation("dev.brewkits:krelay-testing") // Optional: Test fakes and assertions
}
}1. Define a contract in commonMain
interface ToastFeature : RelayFeature {
fun show(message: String)
}2. Dispatch from your ViewModel
class LoginViewModel : ViewModel() {
fun onLoginSuccess() {
KRelay.dispatch<ToastFeature> { it.show("Welcome back!") }
// Zero platform imports. Zero leaks. Queued if the UI isn't ready yet.
}
}3. Register the platform implementation
// Android — Activity or Composable
KRelay.register<ToastFeature>(object : ToastFeature {
override fun show(message: String) =
Toast.makeText(this@MainActivity, message, Toast.LENGTH_SHORT).show()
})// iOS — Swift
let toastClass = KRelayKClassHelpersKt.toastFeatureKClass()
KRelayIosHelperKt.registerFeature(
instance: KRelay.shared.instance,
kClass: toastClass,
impl: IOSToast(viewController: self)
)That's all the wiring needed. KRelay routes the call to the Main Thread, replays it if the UI wasn't ready, and releases the implementation when it's GC'd.
ViewModel KRelay Platform
─────────────────────────────────────────────────────────────
dispatch<Toast> { ... } ──► impl registered?
├── yes: runOnMain { block(impl) }
└── no: sticky queue ──► replay on register()
Three guarantees, always active:
onDestroy cleanup needed for 99% of cases.dispatch is called from, the block executes on Android's Looper.mainLooper() / iOS's GCD main queue.The API is identical on the global singleton and on any isolated instance.
// Registration
KRelay.register<ToastFeature>(impl)
KRelay.unregister<ToastFeature>() // unconditional
KRelay.unregister<ToastFeature>(impl) // identity-safe (won't clear a newer registration)
KRelay.isRegistered<ToastFeature>()
// Dispatch
KRelay.dispatch<ToastFeature> { it.show("Hello") }
KRelay.dispatchWithPriority<ToastFeature>(ActionPriority.CRITICAL) { it.show("Error!") }
// Queue management
KRelay.getPendingCount<ToastFeature>()
KRelay.clearQueue<ToastFeature>()
// Scope tokens — cancel queued actions by caller identity
val token = scopedToken()
KRelay.dispatch<ToastFeature>(token) { it.show("...") }
KRelay.cancelScope(token) // in ViewModel.onCleared()
// Debug
KRelay.dump()
KRelay.debugMode = trueWhen multiple actions queue up before an implementation registers, higher-priority actions replay first. On overflow, the lowest-priority action is evicted (not just the oldest).
KRelay.dispatchWithPriority<NavFeature>(ActionPriority.HIGH) { it.goToHome() }
KRelay.dispatchWithPriority<NavFeature>(ActionPriority.CRITICAL) { it.showError("Timeout") }
// ActionPriority: LOW(0) NORMAL(50) HIGH(100) CRITICAL(1000)Survives process death. The action is saved to SharedPreferences (Android) or NSUserDefaults (iOS) and restored on next launch.
// Register a factory to reconstruct the action from its payload
instance.registerActionFactory<ToastFeature>("toast", "show") { payload ->
{ feature -> feature.show(payload) }
}
// Dispatch — persisted to disk if no impl is available
instance.dispatchPersisted<ToastFeature>("toast", "show", "Payment received")
// On app restart — restores actions into the in-memory queue
instance.restorePersistedActions()Use an explicit string
featureKey(not the class name) — class names can be obfuscated by ProGuard/R8.
The singleton is fine for small apps. For multi-module projects or Koin/Hilt injection, create isolated instances:
// Each module owns its registry — no cross-module interference
val rideKRelay = KRelay.create("Rides")
val foodKRelay = KRelay.create("Food")
// Or with custom settings via builder
val krelay = KRelay.builder("Payment")
.maxQueueSize(50)
.actionExpiry(60_000L)
.debugMode(BuildConfig.DEBUG)
.build()Inject into ViewModels via Koin:
val appModule = module {
single { KRelay.create("AppScope") }
viewModel { LoginViewModel(krelay = get()) }
}
class LoginViewModel(private val krelay: KRelayInstance) : ViewModel() {
fun onSuccess() { krelay.dispatch<NavFeature> { it.goToHome() } }
}Add krelay-compose and use the built-in helpers:
// Registers when composition enters, unregisters when it leaves
@Composable
fun HomeScreen() {
val context = LocalContext.current
KRelayEffect<ToastFeature> {
object : ToastFeature {
override fun show(message: String) =
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
// ...
}// When you need to use the implementation in the same composable
@Composable
fun HomeScreen() {
val snackbarState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
rememberKRelayImpl<ToastFeature> {
object : ToastFeature {
override fun show(message: String) {
scope.launch { snackbarState.showSnackbar(message) }
}
}
}
Scaffold(snackbarHost = { SnackbarHost(snackbarState) }) { ... }
}Both helpers accept an optional instance parameter for the Instance API:
KRelayEffect<ToastFeature>(instance = myKRelayInstance) { ... }Manual
DisposableEffect? Always hoist the implementation intoremember {}. Without it, Kotlin/Native's GC can collect the object before the first dispatch.See Compose Integration Guide for full patterns including Navigation Compose and Voyager.
KRelay is designed for maximum testability. The krelay-testing artifact provides test fakes, JUnit rules, and type-safe assertions, completely removing the need for mocking frameworks.
import dev.brewkits.krelay.testing.KRelayTestRule
import kotlin.test.Test
import kotlin.test.AfterTest
class LoginViewModelTest {
// Automatically resets state after each test
private val relayRule = KRelayTestRule()
private val viewModel by lazy { LoginViewModel(krelay = relayRule.relay) }
@AfterTest
fun tearDown() = relayRule.after()
@Test
fun `login success shows toast and navigates`() {
// Act
viewModel.onLoginSuccess()
// Assert - type-safe and precise
relayRule.relay.assertDispatched<ToastFeature>()
relayRule.relay.assertDispatched<NavFeature>()
// Optional: execute the dispatch against a mock to verify parameters
var toastMessage: String? = null
relayRule.relay.executeLastDispatch(object : ToastFeature {
override fun show(msg: String) { toastMessage = msg }
})
assertEquals("Welcome back!", toastMessage)
}
}Run the test suite:
./gradlew :shared:test # JVM (fast)
./gradlew :shared:iosSimulatorArm64Test # iOS Simulator
./gradlew :shared:connectedDebugAndroidTest # Real Android deviceBy default, three passive protections apply to every queued action:
| Protection | Default | Behaviour |
|---|---|---|
WeakReference |
Always on | Platform impls released when GC'd — no onDestroy cleanup needed |
actionExpiryMs |
5 min | Queued actions expire and are dropped automatically |
maxQueueSize |
100 | When full, lowest-priority (or oldest) action is evicted |
For granular control, use scope tokens to cancel only the actions queued by a specific ViewModel:
class MyViewModel : ViewModel() {
private val token = scopedToken()
fun doWork() = KRelay.dispatch<WorkFeature>(token) { it.run() }
override fun onCleared() = KRelay.cancelScope(token)
}KRelay is framework-agnostic. It connects to whatever navigation, media, or permission library you already use — ViewModels stay clean of all framework imports.
| Category | Library |
|---|---|
| Navigation | Voyager · Decompose · Navigation Compose |
| Media | Peekaboo (image/camera picker) |
| Permissions | Moko Permissions |
| Biometrics | Moko Biometry |
| Reviews | Play Core · StoreKit |
| DI | Koin · Hilt |
See Integration Guides for step-by-step examples.
| KRelay | Kotlin | AGP | Android minSdk | iOS | Desktop (JVM) | WasmJs |
|---|---|---|---|---|---|---|
| 2.2.x | 2.1.x | 8.x | 24 | 14.0+ | ✅ | ✅ |
| 2.1.x | 2.1.x | 8.x | 24 | 14.0+ | — | — |
| 2.0.x | 2.1.x | 8.x | 24 | 14.0+ | — | — |
| 1.1.x | 2.0.x | 8.x | 23 | 13.0+ | — | — |
| 1.0.x | 1.9.x | 7.x | 21 | 13.0+ | — | — |
Platforms: Android arm64 · Android x86_64 · iOS arm64 (device) · iOS arm64 (simulator) · iOS x64 (simulator) · JVM Desktop (macOS, Windows, Linux) · WasmJs Browser
krelay-testing artifact — FakeKRelayInstance with a full assertion API (assertDispatched, assertNotDispatched, executeLastDispatch) for clean, mock-free unit testing.krelay-bom (Bill of Materials) — automatically align versions across all KRelay artifacts.dispatchWithPriority API — simplified prioritization on both the singleton and KRelayInstance.apiCheck) to mathematically guarantee zero breaking API changes in minor/patch releases.KRelayEffect<T> and rememberKRelayImpl<T> Compose helpersdispatchPersisted<T>() — survives process deathSharedPreferencesPersistenceAdapter (Android) and NSUserDefaultsPersistenceAdapter (iOS)scopedToken() + cancelScope(token) for fine-grained ViewModel cleanupresetConfiguration() without clearing the registry or queueKRelay.create("ScopeName") — isolated instances per moduleKRelay.builder(...) — configure queue, expiry, and debug mode per instanceKRelayInstance is an interface, injectable via Koin or Hilt| Guide | Description |
|---|---|
| Compose Integration |
KRelayEffect, rememberKRelayImpl, Navigation Compose, Voyager |
| SwiftUI Integration | iOS-specific patterns, XCTest |
| Integration Guides | Voyager, Decompose, Moko, Peekaboo, DI |
| Lifecycle Guide | Activity · Fragment · UIViewController · SwiftUI |
| Testing Guide | Patterns, mocks, instrumented tests |
| Anti-Patterns | What not to do and why |
| Architecture | Internals deep dive |
| API Reference | Full API cheat sheet |
| Managing Warnings | Suppress @OptIn at module level |
| Migration to v2.0 | Upgrading from v1.x |
Copyright 2026 Brewkits
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Made with care by Nguyễn Tuấn Việt · Brewkits
Dispatch UI commands (Toast, Navigation, Permissions) from shared ViewModels to Android, iOS, Desktop JVM, and Web (WasmJs) — leak-free, rotation-safe, always on the Main Thread.
Most mobile apps treat everything (Toasts, Navigation, Alerts) as State. This is why you see "Ghost Toasts" popping up after rotation, or users stuck because a navigation event fired in the 300ms "blind spot" during an Activity restart.
| Approach | The Pain Point |
|---|---|
Pass Activity / UIViewController
|
Memory leaks and onDestroy boilerplate |
SharedFlow(replay=0) |
Events are lost during screen rotation |
StateFlow as Event |
Double-execution / Side-effects that "stick" |
Channels |
Single-observer only (unsuitable for UI + Analytics) |
KRelay is a Buffered Multicasting bridge. Your shared ViewModel signals an intent, and the platform fulfills it — exactly once, always on the Main Thread, even if the UI wasn't ready when you called it.
"State is for seeing, Event is for running."
KRelay is designed for mission-critical systems (VoIP, Fintech, SOS) where event delivery is non-negotiable.
Read the State vs. Event: Why your MVI/Redux app is probably leaking side-effects.
StateFlow for state, Channel for single-shot UI events) are completely sufficient and introduce zero external dependencies.| Scenario | Better Alternative | Why |
|---|---|---|
| Standard CRUD UI Events |
Channel<UiEvent> / SharedFlow
|
Simpler, standard Kotlin idioms, zero dependencies |
| Need a return value |
suspend fun + expect/actual
|
KRelay is fire-and-forget; cannot return results |
| Reactive UI state |
StateFlow / MutableStateFlow
|
State is for seeing; events are for running |
| Critical side-effects (payment, upload) |
WorkManager / iOS Background Tasks |
KRelay queue is memory-based; lost on process death |
| Database / Network | Room / SQLDelight / Ktor | Core business architecture belongs in Repositories |
When shared business logic must trigger platform APIs tightly coupled to the active Activity or UIViewController lifecycle:
Google Play Core / Apple StoreKit), Native Biometric Prompts (AndroidX Biometric / iOS LocalAuthentication), Photo Picker intents, or Push notification system bridges.expect/actual interfaces through 4–5 architectural layers (ViewModel → UseCase → Repository → Activity) creates fragile boilerplate and lifecycle leaks. With KRelay, the platform Activity or Composable dynamically registers when resumed and unregisters when disposed. KRelay buffers commands in its sticky queue during rotations or screen transitions, dispatching them the moment the native UI is ready.In large, enterprise-scale codebases where independent feature modules run under a single host shell:
Ride) and Team B (Food) running inside the same Super App (e.g., Grab, Uber, Gojek). Modules are strictly forbidden from having compile-time dependencies on each other.KRelay.create("Rides") vs. KRelay.create("Food")) with ScopeToken lifecycle guards. Feature teams dispatch cross-boundary intents dynamically without direct compile-time coupling or global event bus collision.KRelay now provides a Bill of Materials (BOM) to automatically align versions across all artifacts.
// shared/build.gradle.kts
sourceSets {
commonMain.dependencies {
// 1. (Recommended) Import the BOM
api(platform("dev.brewkits:krelay-bom:2.2.0"))
// 2. Add dependencies without specifying versions
implementation("dev.brewkits:krelay")
implementation("dev.brewkits:krelay-compose") // Optional: Compose helpers
implementation("dev.brewkits:krelay-flow") // Optional: Flow operators (v2.2.0+)
}
commonTest.dependencies {
implementation("dev.brewkits:krelay-testing") // Optional: Test fakes and assertions
}
}1. Define a contract in commonMain
interface ToastFeature : RelayFeature {
fun show(message: String)
}2. Dispatch from your ViewModel
class LoginViewModel : ViewModel() {
fun onLoginSuccess() {
KRelay.dispatch<ToastFeature> { it.show("Welcome back!") }
// Zero platform imports. Zero leaks. Queued if the UI isn't ready yet.
}
}3. Register the platform implementation
// Android — Activity or Composable
KRelay.register<ToastFeature>(object : ToastFeature {
override fun show(message: String) =
Toast.makeText(this@MainActivity, message, Toast.LENGTH_SHORT).show()
})// iOS — Swift
let toastClass = KRelayKClassHelpersKt.toastFeatureKClass()
KRelayIosHelperKt.registerFeature(
instance: KRelay.shared.instance,
kClass: toastClass,
impl: IOSToast(viewController: self)
)That's all the wiring needed. KRelay routes the call to the Main Thread, replays it if the UI wasn't ready, and releases the implementation when it's GC'd.
ViewModel KRelay Platform
─────────────────────────────────────────────────────────────
dispatch<Toast> { ... } ──► impl registered?
├── yes: runOnMain { block(impl) }
└── no: sticky queue ──► replay on register()
Three guarantees, always active:
onDestroy cleanup needed for 99% of cases.dispatch is called from, the block executes on Android's Looper.mainLooper() / iOS's GCD main queue.The API is identical on the global singleton and on any isolated instance.
// Registration
KRelay.register<ToastFeature>(impl)
KRelay.unregister<ToastFeature>() // unconditional
KRelay.unregister<ToastFeature>(impl) // identity-safe (won't clear a newer registration)
KRelay.isRegistered<ToastFeature>()
// Dispatch
KRelay.dispatch<ToastFeature> { it.show("Hello") }
KRelay.dispatchWithPriority<ToastFeature>(ActionPriority.CRITICAL) { it.show("Error!") }
// Queue management
KRelay.getPendingCount<ToastFeature>()
KRelay.clearQueue<ToastFeature>()
// Scope tokens — cancel queued actions by caller identity
val token = scopedToken()
KRelay.dispatch<ToastFeature>(token) { it.show("...") }
KRelay.cancelScope(token) // in ViewModel.onCleared()
// Debug
KRelay.dump()
KRelay.debugMode = trueWhen multiple actions queue up before an implementation registers, higher-priority actions replay first. On overflow, the lowest-priority action is evicted (not just the oldest).
KRelay.dispatchWithPriority<NavFeature>(ActionPriority.HIGH) { it.goToHome() }
KRelay.dispatchWithPriority<NavFeature>(ActionPriority.CRITICAL) { it.showError("Timeout") }
// ActionPriority: LOW(0) NORMAL(50) HIGH(100) CRITICAL(1000)Survives process death. The action is saved to SharedPreferences (Android) or NSUserDefaults (iOS) and restored on next launch.
// Register a factory to reconstruct the action from its payload
instance.registerActionFactory<ToastFeature>("toast", "show") { payload ->
{ feature -> feature.show(payload) }
}
// Dispatch — persisted to disk if no impl is available
instance.dispatchPersisted<ToastFeature>("toast", "show", "Payment received")
// On app restart — restores actions into the in-memory queue
instance.restorePersistedActions()Use an explicit string
featureKey(not the class name) — class names can be obfuscated by ProGuard/R8.
The singleton is fine for small apps. For multi-module projects or Koin/Hilt injection, create isolated instances:
// Each module owns its registry — no cross-module interference
val rideKRelay = KRelay.create("Rides")
val foodKRelay = KRelay.create("Food")
// Or with custom settings via builder
val krelay = KRelay.builder("Payment")
.maxQueueSize(50)
.actionExpiry(60_000L)
.debugMode(BuildConfig.DEBUG)
.build()Inject into ViewModels via Koin:
val appModule = module {
single { KRelay.create("AppScope") }
viewModel { LoginViewModel(krelay = get()) }
}
class LoginViewModel(private val krelay: KRelayInstance) : ViewModel() {
fun onSuccess() { krelay.dispatch<NavFeature> { it.goToHome() } }
}Add krelay-compose and use the built-in helpers:
// Registers when composition enters, unregisters when it leaves
@Composable
fun HomeScreen() {
val context = LocalContext.current
KRelayEffect<ToastFeature> {
object : ToastFeature {
override fun show(message: String) =
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
// ...
}// When you need to use the implementation in the same composable
@Composable
fun HomeScreen() {
val snackbarState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
rememberKRelayImpl<ToastFeature> {
object : ToastFeature {
override fun show(message: String) {
scope.launch { snackbarState.showSnackbar(message) }
}
}
}
Scaffold(snackbarHost = { SnackbarHost(snackbarState) }) { ... }
}Both helpers accept an optional instance parameter for the Instance API:
KRelayEffect<ToastFeature>(instance = myKRelayInstance) { ... }Manual
DisposableEffect? Always hoist the implementation intoremember {}. Without it, Kotlin/Native's GC can collect the object before the first dispatch.See Compose Integration Guide for full patterns including Navigation Compose and Voyager.
KRelay is designed for maximum testability. The krelay-testing artifact provides test fakes, JUnit rules, and type-safe assertions, completely removing the need for mocking frameworks.
import dev.brewkits.krelay.testing.KRelayTestRule
import kotlin.test.Test
import kotlin.test.AfterTest
class LoginViewModelTest {
// Automatically resets state after each test
private val relayRule = KRelayTestRule()
private val viewModel by lazy { LoginViewModel(krelay = relayRule.relay) }
@AfterTest
fun tearDown() = relayRule.after()
@Test
fun `login success shows toast and navigates`() {
// Act
viewModel.onLoginSuccess()
// Assert - type-safe and precise
relayRule.relay.assertDispatched<ToastFeature>()
relayRule.relay.assertDispatched<NavFeature>()
// Optional: execute the dispatch against a mock to verify parameters
var toastMessage: String? = null
relayRule.relay.executeLastDispatch(object : ToastFeature {
override fun show(msg: String) { toastMessage = msg }
})
assertEquals("Welcome back!", toastMessage)
}
}Run the test suite:
./gradlew :shared:test # JVM (fast)
./gradlew :shared:iosSimulatorArm64Test # iOS Simulator
./gradlew :shared:connectedDebugAndroidTest # Real Android deviceBy default, three passive protections apply to every queued action:
| Protection | Default | Behaviour |
|---|---|---|
WeakReference |
Always on | Platform impls released when GC'd — no onDestroy cleanup needed |
actionExpiryMs |
5 min | Queued actions expire and are dropped automatically |
maxQueueSize |
100 | When full, lowest-priority (or oldest) action is evicted |
For granular control, use scope tokens to cancel only the actions queued by a specific ViewModel:
class MyViewModel : ViewModel() {
private val token = scopedToken()
fun doWork() = KRelay.dispatch<WorkFeature>(token) { it.run() }
override fun onCleared() = KRelay.cancelScope(token)
}KRelay is framework-agnostic. It connects to whatever navigation, media, or permission library you already use — ViewModels stay clean of all framework imports.
| Category | Library |
|---|---|
| Navigation | Voyager · Decompose · Navigation Compose |
| Media | Peekaboo (image/camera picker) |
| Permissions | Moko Permissions |
| Biometrics | Moko Biometry |
| Reviews | Play Core · StoreKit |
| DI | Koin · Hilt |
See Integration Guides for step-by-step examples.
| KRelay | Kotlin | AGP | Android minSdk | iOS | Desktop (JVM) | WasmJs |
|---|---|---|---|---|---|---|
| 2.2.x | 2.1.x | 8.x | 24 | 14.0+ | ✅ | ✅ |
| 2.1.x | 2.1.x | 8.x | 24 | 14.0+ | — | — |
| 2.0.x | 2.1.x | 8.x | 24 | 14.0+ | — | — |
| 1.1.x | 2.0.x | 8.x | 23 | 13.0+ | — | — |
| 1.0.x | 1.9.x | 7.x | 21 | 13.0+ | — | — |
Platforms: Android arm64 · Android x86_64 · iOS arm64 (device) · iOS arm64 (simulator) · iOS x64 (simulator) · JVM Desktop (macOS, Windows, Linux) · WasmJs Browser
krelay-testing artifact — FakeKRelayInstance with a full assertion API (assertDispatched, assertNotDispatched, executeLastDispatch) for clean, mock-free unit testing.krelay-bom (Bill of Materials) — automatically align versions across all KRelay artifacts.dispatchWithPriority API — simplified prioritization on both the singleton and KRelayInstance.apiCheck) to mathematically guarantee zero breaking API changes in minor/patch releases.KRelayEffect<T> and rememberKRelayImpl<T> Compose helpersdispatchPersisted<T>() — survives process deathSharedPreferencesPersistenceAdapter (Android) and NSUserDefaultsPersistenceAdapter (iOS)scopedToken() + cancelScope(token) for fine-grained ViewModel cleanupresetConfiguration() without clearing the registry or queueKRelay.create("ScopeName") — isolated instances per moduleKRelay.builder(...) — configure queue, expiry, and debug mode per instanceKRelayInstance is an interface, injectable via Koin or Hilt| Guide | Description |
|---|---|
| Compose Integration |
KRelayEffect, rememberKRelayImpl, Navigation Compose, Voyager |
| SwiftUI Integration | iOS-specific patterns, XCTest |
| Integration Guides | Voyager, Decompose, Moko, Peekaboo, DI |
| Lifecycle Guide | Activity · Fragment · UIViewController · SwiftUI |
| Testing Guide | Patterns, mocks, instrumented tests |
| Anti-Patterns | What not to do and why |
| Architecture | Internals deep dive |
| API Reference | Full API cheat sheet |
| Managing Warnings | Suppress @OptIn at module level |
| Migration to v2.0 | Upgrading from v1.x |
Copyright 2026 Brewkits
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Made with care by Nguyễn Tuấn Việt · Brewkits