Event and State Management in KMP
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
andonChangePassword
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
andpassword
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.