Shared BaseViewModel in Kotlin Multiplatform (KMP)
In this blog post, we'll explore how to create and use a shared BaseViewModel
in a Kotlin Multiplatform (KMP) project. This approach allows us to write shared logic for both Android and iOS platforms, making our codebase more efficient and maintainable.
Common Shared Code
The common shared code consists of abstract and interface definitions that are expected to be implemented differently on each platform. Let's break down each component step by step.
1. Abstract ViewModel
Class
expect abstract class ViewModel() {
val coroutine: CoroutineScope
protected open fun onCleared()
}
This abstract class defines a ViewModel
with a CoroutineScope
and an onCleared
method. The expect
keyword indicates that platform-specific implementations will be provided.
2. Event
and State
Interfaces
interface Event {}
interface State {}
These interfaces are used to define events and states that the ViewModel
will handle.
3. StateViewModel
Class
abstract class StateViewModel<E : Event, S : State>(initialState: S) : ViewModel() {
private val _event = Channel<E>()
val event = _event.receiveAsFlow().shareIn(CoroutineScope(Dispatchers.Main), SharingStarted.Lazily).toCommonFlow()
private val _state = MutableStateFlow(initialState)
val state = _state.asStateFlow().toCommonStateFlow()
protected suspend fun sendEvent(event: E) = _event.send(event)
@OptIn(DelicateCoroutinesApi::class)
protected fun sendEventSync(event: E) = GlobalScope.launch { _event.send(event) }
protected fun updateState(newState: S) {
_state.value = newState
}
override fun onCleared() {
_event.close()
super.onCleared()
}
}
The StateViewModel
class extends ViewModel
and is parameterized by Event
and State
. It uses Channel
and StateFlow
to handle events and state updates.
sendEvent
andsendEventSync
are used to send events asynchronously and synchronously, respectively.updateState
is used to update the current state.onCleared
is overridden to close the event channel.
4. CommonStateFlow
, CommonFlow
, and CommonMutableStateFlow
Classes
These classes wrap Kotlin StateFlow
, Flow
, and MutableStateFlow
to provide platform-specific implementations.
expect class CommonStateFlow<T>(flow: StateFlow<T>): StateFlow<T>
fun <T> StateFlow<T>.toCommonStateFlow() = CommonStateFlow(this)
expect class CommonFlow<T>(flow: Flow<T>): Flow<T>
fun <T> Flow<T>.toCommonFlow() = CommonFlow(this)
expect class CommonMutableStateFlow<T>(flow: MutableStateFlow<T>): MutableStateFlow<T>
fun <T> MutableStateFlow<T>.toCommonMutableStateFlow() = CommonMutableStateFlow(this)
iOS Module
The iOS-specific implementations of the common shared code are as follows:
actual open class CommonFlow<T> actual constructor(private val flow: Flow<T>): Flow<T> by flow {
fun subscribe(coroutineScope: CoroutineScope, dispatcher: CoroutineDispatcher, onCollect: (T) -> Unit): DisposableHandle {
val job = coroutineScope.launch(dispatcher) {
flow.collect(onCollect)
}
return DisposableHandle { job.cancel() }
}
fun subscribe(onCollect: (T) -> Unit): DisposableHandle {
return subscribe(coroutineScope = GlobalScope, dispatcher = Dispatchers.Main, onCollect = onCollect)
}
}
actual open class CommonMutableStateFlow<T> actual constructor(private val flow: MutableStateFlow<T>)
: CommonStateFlow<T>(flow), MutableStateFlow<T> {
override var value: T
get() = super.value
set(value) { flow.value = value }
override val subscriptionCount: StateFlow<Int>
get() = flow.subscriptionCount
override fun compareAndSet(expect: T, update: T): Boolean = flow.compareAndSet(expect, update)
@ExperimentalCoroutinesApi
override fun resetReplayCache() { flow.resetReplayCache() }
override fun tryEmit(value: T): Boolean = flow.tryEmit(value)
override suspend fun emit(value: T) { flow.emit(value) }
}
actual open class CommonStateFlow<T> actual constructor(private val flow: StateFlow<T>)
: CommonFlow<T>(flow), StateFlow<T> {
override val replayCache: List<T>
get() = flow.replayCache
override val value: T
get() = flow.value
override suspend fun collect(collector: FlowCollector<T>): Nothing {
flow.collect(collector)
}
}
fun interface DisposableHandle: kotlinx.coroutines.DisposableHandle
These classes provide implementations for subscribing to Flow
and handling StateFlow
and MutableStateFlow
on iOS.
Android Module
The Android-specific implementations of the common shared code are as follows:
actual abstract class ViewModel actual constructor() : AndroidXViewModel() {
actual override fun onCleared() {
super.onCleared()
}
actual val coroutine = viewModelScope
}
actual class CommonFlow<T> actual constructor(private val flow: Flow<T>) : Flow<T> by flow
actual class CommonMutableStateFlow<T> actual constructor(private val flow: MutableStateFlow<T>)
: MutableStateFlow<T> by flow
actual class CommonStateFlow<T> actual constructor(private val flow: StateFlow<T>)
: StateFlow<T> by flow
These implementations utilize Android's viewModelScope
and extend AndroidXViewModel
.
How to use this SharedViewModel while managing state and event for android and iOS both.
Checkout this next part of this blog : https://engineering.codefylabs.com/event-and-state-management-in-kmp
Conclusion
Integrating a shared BaseViewModel
in a KMP project allows you to share business logic between Android and iOS, reducing code duplication and maintenance efforts. By defining expected classes and interfaces in the common module and providing platform-specific implementations, you can effectively manage state and events in a multiplatform environment.
By following the structure and code provided in this blog, you should be able to create a robust and scalable architecture for your KMP project. Happy coding!