Event and State Management in KMP

Codefy Labs's photo
·

4 min read

In this blog post, we'll delve into creating a shared LoginViewModel using Kotlin Multiplatform (KMP), enabling us to write shared business logic for both Android and iOS platforms. We'll also cover the platform-specific UI implementations using Jetpack Compose for Android and SwiftUI for iOS.

Prerequisite: If you haven't checked out our "Base ViewModel for iOS & Android" blog yet, please read that first for a better understanding before proceeding with this guide..

Check it out : https://engineering.codefylabs.com/shared-baseviewmodel-in-kotlin-multiplatform-kmp

Common Module: Shared Logic

The common module contains the shared LoginViewModel and supporting classes.

1. LoginViewModel Class

The LoginViewModel class manages the state and events related to the login process.

class LoginViewModel(
    private val loginUseCase: LoginUseCase,
    val sessionUseCase: SessionUseCase
) : StateViewModel<LoginEvent, LoginViewState>(LoginViewState.initial()) {

    fun onChangeEmailId(email: String) =
        updateState(state.value.copy(emailId = email))

    fun onChangePassword(password: String) =
        updateState(state.value.copy(password = password))

    suspend fun login() {
        updateState(state.value.copy(isLoading = true))

        try {
            // API call for login
            sendEvent(LoginEvent.OnAuthenticated("Login Successful"))
        } catch (e: Exception) {
            sendEvent(LoginEvent.ShowMessage(e.message ?: "Unknown error"))
        } finally {
            updateState(state.value.copy(isLoading = false))
        }
    }
}
  • onChangeEmailId and onChangePassword update the state with the new email and password.

  • login handles the login process, sending events based on the result.

2. LoginViewState Data Class

This data class represents the state of the login screen.

data class LoginViewState(
    val isLoading: Boolean = false,
    val emailId: String = "",
    val password: String = ""
) : State {
    companion object {
        fun initial() = LoginViewState()
    }
}
  • isLoading indicates if the login process is ongoing.

  • emailId and password store the user's input.

3. LoginEvent Sealed Class

Events triggered by the LoginViewModel.

sealed class LoginEvent : Event {
    data class OnAuthenticated(val message: String) : LoginEvent()
    data class ShowMessage(val message: String) : LoginEvent()
}
  • OnAuthenticated is triggered on successful login.

  • ShowMessage is used to display any messages.

Android Module: UI with Jetpack Compose

In the Android module, we use Jetpack Compose to create the UI for the login screen.

1. OnEvent Composable Function

This composable function handles collecting events from the ViewModel.

@Composable
fun <E : Event> OnEvent(event: Flow<E>, onEvent: (E) -> Unit) {
    LaunchedEffect(Unit) {
        event.collect(onEvent)
    }
}

2. LoginScreen Composable Function

This composable function defines the login screen UI.

@Composable
fun LoginScreen(navigateToHome: () -> Unit, viewModel: LoginViewModel = viewModel()) {
    val context = LocalContext.current
    val state by viewModel.state.collectAsState()
    val coroutine = rememberCoroutineScope()

    OnEvent(event = viewModel.event, onEvent = {
        when (it) {
            is LoginEvent.ShowMessage -> context.toast(it.message)
            is LoginEvent.OnAuthenticated -> {
                context.toast(it.message)
                navigateToHome()
            }
        }
    })

    Column {
        TextField(
            value = state.emailId,
            onValueChange = viewModel::onChangeEmailId,
            label = { Text("Email") }
        )
        TextField(
            value = state.password,
            onValueChange = viewModel::onChangePassword,
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation()
        )
        Button(onClick = { coroutine.launch ( viewModel.login() ) }) {
            Text("Login")
        }
    }
}

iOS Module: UI with SwiftUI

In the iOS module, we use SwiftUI to create the UI for the login screen.

1. LoginIOSViewModel Class

This class bridges the shared LoginViewModel with SwiftUI.

import Foundation
import shared

class LoginIOSViewModel : ObservableObject {
    private let sharedVM: LoginViewModel = ViewModelProvider.shared.getLoginViewModel()
    @Published var loginState: LoginViewState = LoginViewState.companion.initial()
    private var disposableHandle: DisposableHandle?

    init(sharedLoginViewModel: LoginViewModel) {
        self.sharedVM = sharedLoginViewModel
        observeEvents()
    }

    private func observeEvents() {
        sharedVM.event.subscribe(onCollect: { [weak self] e in
            guard let self = self else { return }
            if let event = e {
                DispatchQueue.main.async {
                    switch event {
                    case let success as LoginEvent.OnAuthenticated:
                        ToastManager.shared.show(toast: Toast(style: .success, message: success.message))
                        NotificationPusherManager.shared.configurePusherWithUser()
                    case let showMessage as LoginEvent.ShowMessage:
                        ToastManager.shared.show(message: showMessage.message)
                    default: break
                    }
                }
            }
        })
    }

    func observeState() {
        disposableHandle = sharedVM.state.subscribe(onCollect: { [weak self] newState in
            guard let self = self else { return }
            DispatchQueue.main.async {
                self.loginState = newState
            }
        })
    }

    func onEmailChange(value: String) {
        sharedVM.onChangeEmailId(email: value)
    }

    func onPasswordChange(value: String) {
        sharedVM.onChangePassword(password: value)
    }

    func login() {
        sharedVM.login()
    }

    deinit {
        disposableHandle?.dispose()
    }
}

2. SwiftUI View

The SwiftUI view for the login screen.

import SwiftUI

struct LoginView: View {
    @ObservedObject var viewModel: LoginIOSViewModel

    var body: some View {
        VStack {
            TextField("Email", text: Binding(
                get: { viewModel.loginState.emailId },
                set: viewModel.onEmailChange
            ))
            .padding()
            .textFieldStyle(RoundedBorderTextFieldStyle())

            SecureField("Password", text: Binding(
                get: { viewModel.loginState.password },
                set: viewModel.onPasswordChange
            ))
            .padding()
            .textFieldStyle(RoundedBorderTextFieldStyle())

            if viewModel.loginState.isLoading {
                ProgressView()
                    .padding()
            }

            Button(action: viewModel.login) {
                Text("Login")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .task{
             viewModel.observeState()
         }
        .padding()
    }
}

Conclusion

By leveraging Kotlin Multiplatform, we can share the business logic of our LoginViewModel between Android and iOS, reducing code duplication and ensuring consistency. The platform-specific UI implementations with Jetpack Compose and SwiftUI provide a seamless user experience across both platforms. This approach not only simplifies maintenance but also accelerates development by reusing a significant portion of the codebase.