// In your shared module's build.gradle.kts
commonMain.dependencies {
implementation(project(":krelay"))
}interface ToastFeature : RelayFeature {
fun show(message: String)
}class MyViewModel {
fun onSuccess() {
KRelay.dispatch<ToastFeature> {
it.show("Success!")
}
}
}Android:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
KRelay.register<ToastFeature>(
object : ToastFeature {
override fun show(msg: String) {
Toast.makeText(this@MainActivity, msg, Toast.LENGTH_SHORT).show()
}
}
)
}
}iOS (Swift):
class ContentView: UIView {
func setup() {
KRelay.shared.register(
impl: object: ToastFeature {
func show(message: String) {
// Show iOS alert
}
}
)
}
}// In Application.onCreate() or setup
KRelay.debugMode = true // Enable logging
KRelay.maxQueueSize = 100 // Max actions per feature
KRelay.actionExpiryMs = 5 * 60 * 1000 // 5 minutes
KRelay.metricsEnabled = true // Enable metrics// Register implementation
KRelay.register<ToastFeature>(impl)
// Dispatch action
KRelay.dispatch<ToastFeature> { it.show("Hello") }
// Check if registered
val isRegistered = KRelay.isRegistered<ToastFeature>()
// Get pending count
val pending = KRelay.getPendingCount<ToastFeature>()
// Clear queue
KRelay.clearQueue<ToastFeature>()
// Unregister
KRelay.unregister<ToastFeature>()
// Reset everything
KRelay.reset()// Print visual system state to console
KRelay.dump()
// Get diagnostic data programmatically
val info: DebugInfo = KRelay.getDebugInfo()
// Returns: registeredFeaturesCount, registeredFeatures, featureQueues,
// totalPendingActions, expiredActionsRemoved, maxQueueSize, etc.
// Get count of registered features
val count = KRelay.getRegisteredFeaturesCount()
// Get total pending actions across all features
val totalPending = KRelay.getTotalPendingCount()import dev.brewkits.krelay.ActionPriority
// Dispatch with priority
KRelay.dispatchWithPriority<ErrorFeature>(ActionPriority.CRITICAL) {
it.show("Critical error!")
}
// Priority levels
ActionPriority.LOW // Analytics, logging
ActionPriority.NORMAL // Default
ActionPriority.HIGH // Important notifications
ActionPriority.CRITICAL // Security alertsclass MyUseCase {
suspend fun execute() {
try {
// Business logic
} catch (e: Exception) {
KRelay.dispatchWithPriority<ErrorFeature>(ActionPriority.HIGH) {
it.show("Error: ${e.message}")
}
}
}
}class LoginViewModel {
suspend fun login(username: String, password: String) {
val result = authService.login(username, password)
if (result.isSuccess) {
KRelay.dispatch<ToastFeature> { it.show("Welcome back!") }
KRelay.dispatch<NavigationFeature> { it.navigateTo("home") }
}
}
}class DataSyncService {
suspend fun syncData() {
// This runs in background
val data = api.fetchData()
// UI update automatically on main thread
KRelay.dispatch<ToastFeature> {
it.show("Sync complete: ${data.size} items")
}
}
}class CameraViewModel {
fun takePicture() {
KRelay.dispatch<PermissionFeature> {
it.requestCamera { granted ->
if (granted) startCamera()
}
}
}
}class GameViewModel {
fun onScored() {
KRelay.dispatch<HapticFeature> {
it.vibrate(duration = 100)
}
}
}class CheckoutViewModel {
fun completeOrder() {
KRelay.dispatch<AnalyticsFeature> {
it.track("order_completed")
}
}
}// ViewModel dispatches during rotation
class MyViewModel {
fun onDataLoaded() {
KRelay.dispatch<ToastFeature> { it.show("Loaded!") }
}
}
// After rotation, new Activity auto-registers
// Queued toast is shown automatically!// WRONG: Can't get return value
fun getBattery(): Int {
var level = 0
KRelay.dispatch<BatteryFeature> { level = it.level }
return level // Returns 0, not actual level!
}
// CORRECT: Use expect/actual or callbacks
expect fun getBatteryLevel(): Int// WRONG: Don't use KRelay for state
KRelay.dispatch<StateFeature> { it.update(newState) }
// CORRECT: Use StateFlow
val uiState = MutableStateFlow(UiState())// WRONG: Main thread blocks UI!
KRelay.dispatch<ProcessingFeature> { it.processLargeFile() }
// CORRECT: Use Dispatchers.IO
viewModelScope.launch(Dispatchers.IO) {
processLargeFile()
withContext(Main) {
KRelay.dispatch<ToastFeature> { it.show("Done!") }
}
}// WRONG: Lost on process death
KRelay.dispatch<PaymentFeature> { it.sendPayment() }
// CORRECT: Use WorkManager
val work = OneTimeWorkRequest<PaymentWorker>()
WorkManager.enqueue(work)// Aggressive cleanup (low memory devices)
KRelay.maxQueueSize = 20
KRelay.actionExpiryMs = 60 * 1000 // 1 minute
// Conservative (important actions)
KRelay.maxQueueSize = 200
KRelay.actionExpiryMs = 10 * 60 * 1000 // 10 minutes
// Immediate discard (no queueing)
KRelay.maxQueueSize = 0
// Unlimited (v1.0.0 behavior - not recommended)
KRelay.maxQueueSize = Int.MAX_VALUE// Development
if (BuildConfig.DEBUG) {
KRelay.debugMode = true
KRelay.metricsEnabled = true
}
// Production
KRelay.debugMode = false
KRelay.metricsEnabled = true // For analytics// Is feature registered?
if (!KRelay.isRegistered<ToastFeature>()) {
println("ToastFeature not registered!")
}
// How many actions queued?
val pending = KRelay.getPendingCount<ToastFeature>()
if (pending > 10) {
println("Warning: $pending actions queued")
}
// View metrics
val metrics = KRelay.getMetrics<ToastFeature>()
println("Dispatches: ${metrics["dispatches"]}")
println("Expired: ${metrics["expired"]}")Issue: Actions not executing
// Solution 1: Check if registered
assertTrue(KRelay.isRegistered<ToastFeature>())
// Solution 2: Check queue
val pending = KRelay.getPendingCount<ToastFeature>()
if (pending > 0) {
// Actions queued, waiting for register
KRelay.register<ToastFeature>(impl)
}Issue: Queue growing too large
// Solution: Reduce limits
KRelay.maxQueueSize = 20
KRelay.actionExpiryMs = 60 * 1000
// Or: Clear queue
KRelay.clearQueue<ToastFeature>()Issue: Memory leak suspected
// Solution: Check metrics
val metrics = KRelay.getMetrics<ToastFeature>()
if (metrics["queued"]!! > metrics["replayed"]!!) {
// More queued than replayed - potential issue
// Check if you're calling register()
}// Best practice: Register in Activity/Fragment
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ✅ Good: Application context (long-lived)
KRelay.register<ToastFeature>(AndroidToast(applicationContext))
// ⚠️ Avoid: Activity context (might leak)
// KRelay.register<ToastFeature>(AndroidToast(this))
}
}
// Optional: Explicit cleanup
override fun onDestroy() {
super.onDestroy()
KRelay.unregister<ToastFeature>() // Though WeakRef handles this
}// Register in View lifecycle
struct ContentView: View {
var body: some View {
ComposeView()
.onAppear {
let impl = IOSToastFeature(viewController: getVC())
KRelay.shared.register(impl: impl)
}
}
}
// Or in UIViewController
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
KRelay.shared.register(impl: IOSToastFeature(viewController: self))
}
}-
Use Priority for Critical Actions
KRelay.dispatchWithPriority<ErrorFeature>(ActionPriority.CRITICAL) { it.show("Payment failed!") }
-
Configure Queue Limits
KRelay.maxQueueSize = 50 // Prevent unbounded growth
-
Enable Metrics in Debug Only
KRelay.metricsEnabled = BuildConfig.DEBUG
-
Clear Queues When Navigating Away
override fun onPause() { super.onPause() KRelay.clearQueue<TransientFeature>() // Don't replay when returning }
| Task | Code |
|---|---|
| Register | KRelay.register<T>(impl) |
| Dispatch | KRelay.dispatch<T> { ... } |
| Dispatch with priority | KRelay.dispatchWithPriority<T>(priority) { ... } |
| Check registered | KRelay.isRegistered<T>() |
| Get pending count | KRelay.getPendingCount<T>() |
| Clear queue | KRelay.clearQueue<T>() |
| Unregister | KRelay.unregister<T>() |
| Reset all | KRelay.reset() |
| Get metrics | KRelay.getMetrics<T>() |
| Print metrics | KRelayMetrics.printReport() |
1. 🛡️ No Memory Leaks (Automatic WeakReference)
- DIY solutions forget to clear strong references → OutOfMemoryError
- KRelay uses WeakReference internally → Auto-cleanup
- Activities/ViewControllers properly garbage collected
- Zero manual lifecycle management needed
2. 📦 Sticky Queue (Never Lose Commands)
- DIY solutions drop commands when UI isn't ready
- KRelay queues commands and auto-replays when UI registers
- Survives screen rotation, cold start, background→foreground
- Commands execute even if dispatched before Activity created
- 🧵 Thread Safety: All actions execute on UI thread automatically
- 🎯 Fire-and-Forget: Just dispatch, don't worry about lifecycle
- ✅ Type-Safe: Compile-time checking, no string keys
Queue does NOT survive app process death. Lambdas cannot be serialized.
✅ Good Use Cases:
- UI commands (Toast, Navigation, Dialog)
- Screen refresh triggers
- Non-critical analytics
❌ Bad Use Cases:
- Critical transactions (payments, orders)
- Important data operations
- Operations requiring guaranteed execution
Use Instead:
- WorkManager - Critical background work
- SavedStateHandle - UI state persistence
- Room/DataStore - Data persistence
Global object KRelay - simple but has trade-offs:
- Pro: Zero configuration, global access
- Con: Testing requires
reset(), hard to isolate in Super Apps
Workaround: Use feature namespacing for module isolation
Perfect For:
- Native interop from Kotlin Multiplatform
- UI commands and feedback
- Fire-and-forget operations
- Screen rotation handling
Not For:
- Business logic requiring guaranteed execution
- Critical data operations
- Operations needing process death survival
- README.md - Complete guide with examples
- ARCHITECTURE.md - Internal design and implementation details
- TESTING.md - Testing guide and best practices
Version: 1.0.1 Status: Production Ready ✅ License: Apache 2.0 Last Updated: 2026-01-23