Implementation guide for Event-Driven UI in Kotlin Multiplatform (Android & iOS).
The server 100% supports Android & iOS because:
- Firebase Cloud Messaging supports multi-platform
- Server includes AndroidConfig & ApnsConfig
- Same API endpoints for all platforms
- Same event types for all platforms
shared/
├── commonMain/
│ └── kotlin/com/amary/pay/
│ ├── data/ # Data layer
│ │ ├── local/ # DataStore
│ │ ├── model/ # Data models
│ │ ├── remote/ # API Service (Ktor Client)
│ │ └── repository/ # Repository implementations
│ ├── domain/ # Domain layer
│ │ ├── model/ # Domain models
│ │ ├── repository/ # Repository interfaces
│ │ └── usecase/ # Use cases
│ ├── presentation/ # Presentation layer
│ │ ├── confirm/ # ConfirmPaymentViewModel
│ │ └── status/ # StatusPaymentViewModel, AnimationStage
│ ├── fcm/ # FCM handling
│ │ ├── EventType.kt # Event types enum
│ │ ├── NotificationData.kt # Notification data model
│ │ ├── FCMManager.kt # expect declaration
│ │ ├── AppStateManager.kt # State management
│ │ └── EventHandler.kt # Event handling logic
│ └── di/ # Dependency injection
│ └── AppModule.kt # Koin modules
├── androidMain/
│ └── kotlin/com/amary/pay/
│ ├── fcm/FCMManager.android.kt # Android FCM implementation
│ └── data/remote/BaseUrl.android.kt
└── iosMain/
└── kotlin/com/amary/pay/
├── fcm/FCMManager.ios.kt # iOS FCM implementation
└── data/remote/BaseUrl.ios.kt
// shared/src/commonMain/kotlin/com/amary/pay/fcm/EventType.kt
enum class EventType {
BALANCE_UPDATED,
PAYMENT_SUCCESS,
PAYMENT_FAILED,
TRANSACTION_CREATED,
REFRESH_HOME,
REFRESH_TRANSACTIONS,
NEW_PROMOTION,
SECURITY_ALERT
}
// shared/src/commonMain/kotlin/com/amary/pay/fcm/NotificationData.kt
@Serializable
data class NotificationData(
val eventType: String,
val timestamp: Long,
val data: Map<String, String> = emptyMap()
)// shared/src/commonMain/kotlin/com/amary/pay/fcm/EventHandler.kt
class EventHandler(
private val stateManager: AppStateManager
) {
fun handleNotification(eventType: String, data: Map<String, String>) {
println("Handling event: $eventType with data: $data")
when (eventType) {
"BALANCE_UPDATED" -> {
val newBalance = data["newBalance"]?.toLongOrNull() ?: 0
stateManager.updateBalance(newBalance)
}
"PAYMENT_SUCCESS" -> {
val transactionId = data["transactionId"] ?: ""
val amount = data["amount"]?.toLongOrNull() ?: 0
stateManager.addTransaction(transactionId, amount)
}
"REFRESH_HOME" -> {
stateManager.triggerHomeRefresh()
}
"REFRESH_TRANSACTIONS" -> {
stateManager.triggerTransactionsRefresh()
}
"NEW_PROMOTION" -> {
val promoCode = data["promoCode"] ?: ""
stateManager.addPromotion(promoCode)
}
"SECURITY_ALERT" -> {
val activity = data["activity"] ?: ""
stateManager.showSecurityAlert(activity)
}
else -> {
println("Unknown event type: $eventType")
}
}
}
}// shared/src/commonMain/kotlin/com/amary/pay/fcm/AppStateManager.kt
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class AppStateManager {
// Balance state
private val _balance = MutableStateFlow<Long>(0)
val balance: StateFlow<Long> = _balance.asStateFlow()
// Refresh triggers
private val _shouldRefreshHome = MutableStateFlow(false)
val shouldRefreshHome: StateFlow<Boolean> = _shouldRefreshHome.asStateFlow()
private val _shouldRefreshTransactions = MutableStateFlow(false)
val shouldRefreshTransactions: StateFlow<Boolean> = _shouldRefreshTransactions.asStateFlow()
// Promotions
private val _promotions = MutableStateFlow<List<String>>(emptyList())
val promotions: StateFlow<List<String>> = _promotions.asStateFlow()
// Security alerts
private val _securityAlerts = MutableStateFlow<List<String>>(emptyList())
val securityAlerts: StateFlow<List<String>> = _securityAlerts.asStateFlow()
fun updateBalance(newBalance: Long) {
_balance.value = newBalance
println("Balance updated to: $newBalance")
}
fun addTransaction(transactionId: String, amount: Long) {
println("Transaction added: $transactionId, amount: $amount")
// Trigger transaction list refresh
triggerTransactionsRefresh()
}
fun triggerHomeRefresh() {
_shouldRefreshHome.value = true
println("Home refresh triggered")
}
fun triggerTransactionsRefresh() {
_shouldRefreshTransactions.value = true
println("Transactions refresh triggered")
}
fun addPromotion(promoCode: String) {
_promotions.value = _promotions.value + promoCode
println("Promotion added: $promoCode")
}
fun showSecurityAlert(activity: String) {
_securityAlerts.value = _securityAlerts.value + activity
println("Security alert: $activity")
}
fun resetRefreshFlags() {
_shouldRefreshHome.value = false
_shouldRefreshTransactions.value = false
}
}// shared/src/commonMain/kotlin/com/amary/pay/fcm/FCMManager.kt
expect class FCMManager {
fun initialize()
fun getToken(callback: (String?) -> Unit)
fun subscribeToTopic(topic: String)
fun setEventHandler(handler: (eventType: String, data: Map<String, String>) -> Unit)
}// shared/src/androidMain/kotlin/com/amary/pay/fcm/FCMManager.android.kt
import com.google.firebase.messaging.FirebaseMessaging
import android.content.Context
actual class FCMManager(private val context: Context) {
private var eventHandler: ((String, Map<String, String>) -> Unit)? = null
actual fun initialize() {
// Firebase will be initialized in Application class
}
actual fun getToken(callback: (String?) -> Unit) {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
callback(task.result)
} else {
callback(null)
}
}
}
actual fun subscribeToTopic(topic: String) {
FirebaseMessaging.getInstance().subscribeToTopic(topic)
}
actual fun setEventHandler(handler: (eventType: String, data: Map<String, String>) -> Unit) {
this.eventHandler = handler
}
// Called from FirebaseMessagingService
fun handleRemoteMessage(data: Map<String, String>) {
val eventType = data["eventType"] ?: return
eventHandler?.invoke(eventType, data)
}
}// shared/src/iosMain/kotlin/com/amary/pay/fcm/FCMManager.ios.kt
import cocoapods.FirebaseMessaging.FIRMessaging
import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSNotification
actual class FCMManager {
private var eventHandler: ((String, Map<String, String>) -> Unit)? = null
actual fun initialize() {
// Firebase initialized in iOS app
}
actual fun getToken(callback: (String?) -> Unit) {
val token = FIRMessaging.messaging().FCMToken
callback(token)
}
actual fun subscribeToTopic(topic: String) {
FIRMessaging.messaging().subscribeToTopic(topic)
}
actual fun setEventHandler(handler: (eventType: String, data: Map<String, String>) -> Unit) {
this.eventHandler = handler
}
// Called from iOS notification handler
fun handleRemoteMessage(userInfo: Map<Any?, *>) {
val eventType = userInfo["eventType"] as? String ?: return
val data = userInfo.filterKeys { it is String }
.mapKeys { it.key as String }
.mapValues { it.value.toString() }
eventHandler?.invoke(eventType, data)
}
}- Add Firebase to Android app:
// composeApp/build.gradle.kts (androidMain)
dependencies {
implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
implementation("com.google.firebase:firebase-messaging-ktx")
}- Create FirebaseMessagingService:
// composeApp/src/androidMain/kotlin/MyFirebaseMessagingService.kt
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
if (remoteMessage.data.isNotEmpty()) {
// Pass to shared FCMManager singleton
FCMManager.getInstance().handleRemoteMessage(remoteMessage.data)
// Show notification if not silent
val isSilent = remoteMessage.data["silent"]?.toBoolean() ?: false
if (!isSilent) {
showNotification(
remoteMessage.data["title"] ?: "AmaryPay",
remoteMessage.data["body"] ?: ""
)
}
}
}
}- Add Firebase to iOS via CocoaPods:
# iosApp/Podfile
pod 'Firebase/Messaging'- Setup in Swift:
// iosApp/iOSApp.swift
import FirebaseCore
import FirebaseMessaging
@main
struct iOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
Messaging.messaging().delegate = self
UNUserNotificationCenter.current().delegate = self
// Request permission
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
print("Permission granted: \(granted)")
}
application.registerForRemoteNotifications()
return true
}
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("FCM Token: \(fcmToken ?? "")")
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
let userInfo = notification.request.content.userInfo
// Pass to shared event handler
if let eventType = userInfo["eventType"] as? String {
let data = userInfo.compactMapValues { $0 as? String }
// Call shared EventHandler
}
return [[.banner, .sound]]
}
}- Single Event Handling Logic - Write once, use on Android & iOS
- Shared State Management - Consistent state across platforms
- Type Safety - Kotlin type system for both platforms
- Code Reuse - 70-80% code sharing
- Easier Testing - Test shared logic once
- Consistent Behavior - Same logic = same behavior
Server is 100% ready for KMP implementation:
- Support Android (via FCM)
- Support iOS (via FCM + APNS)
- Same API for all platforms
- Same event types
- Ready for shared event handling logic
You just need to implement FCM receiver on each platform and connect to the shared event handler!