Skip to content

Latest commit

 

History

History
396 lines (321 loc) · 12.5 KB

File metadata and controls

396 lines (321 loc) · 12.5 KB

KMP Implementation Guide - Event-Driven UI

Implementation guide for Event-Driven UI in Kotlin Multiplatform (Android & iOS).

Overview

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

KMP Structure for Event-Driven UI

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

Step-by-Step Implementation

1. Shared Models (commonMain)

// 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()
)

2. Shared Event Handler (commonMain)

// 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")
            }
        }
    }
}

3. Shared State Manager (commonMain)

// 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
    }
}

4. Platform Interface (commonMain)

// 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)
}

5. Android Implementation (androidMain)

// 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)
    }
}

6. iOS Implementation (iosMain)

// 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)
    }
}

Platform-Specific Setup

Android Setup

  1. 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")
}
  1. 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"] ?: ""
                )
            }
        }
    }
}

iOS Setup

  1. Add Firebase to iOS via CocoaPods:
# iosApp/Podfile
pod 'Firebase/Messaging'
  1. 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]]
    }
}

Benefits of KMP Approach

  1. Single Event Handling Logic - Write once, use on Android & iOS
  2. Shared State Management - Consistent state across platforms
  3. Type Safety - Kotlin type system for both platforms
  4. Code Reuse - 70-80% code sharing
  5. Easier Testing - Test shared logic once
  6. Consistent Behavior - Same logic = same behavior

Summary

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!