This post was originally posted on Medium.
Note: Our posts will generally be about Kotlin Multiplatform for iOS & Android (KMM)
At Double Symmetry, we’ve been using Kotlin Multiplatform (KMP) for building iOS and Android applications for the last two years. KMP’s ability to share Kotlin code between different platforms has enabled our team to continue building beautiful native UI while sharing our platform independent code. For the last two years, our goal with KMP has been to maximise the amount of code that can be shared between platforms while also aiming to not lose the commodities of each.
The Problem Space
At the moment most apps (we know of) use KMP to share their data/domain layer between platforms.. in this post we’ll take this one step further — bringing sharing to the presentation layer! At Double Symmetry, the presentation layer of the apps we build is built on the MVVM(-C) pattern. Therefore a very important part of our apps and shared code is the ViewModel.
Requirements for a Shared ViewModel
On Android, Jetpack provides a ViewModel
class which is automatically tied to scope (usually a fragment, an activity or a composable) and is retained for as long as that scope is alive. It also makes available a CoroutineScope
that’s tied to this ViewModel
’s lifecycle which can be used for writing your business logic.
If we’re to create a shared view model we’d like the following:
- On Android our
ViewModel
should also be able to leverage lifecycle awareness. - Our ViewModel should provide a
CoroutineScope
that we can use.
Implementation
We’ll start by defining our base ViewModel class that exposes a CoroutineScope as we’d expect from our requirements:
// commonMain
expect open class BaseViewModel() {
val scope: CoroutineScope
}
Simple enough! Now let’s work on our Android definition. The requirement is that we can also leverage lifecycle awareness.. so should we figure out how Jetpack’s ViewModel does it? No need. Let’s use that!
// shared/build.gradle
val androidMain by getting {
dependencies {
// add jetpack dependecy for viewmodel
api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1")
}
}
// androidMain
actual open class BaseViewModel : ViewModel() {
actual val scope = viewModelScope
}
Voila! Now on Android our instances of BaseViewModel will conform to all the sweet commodities we’d expect and exposes a CoroutineScope. But what about iOS?
// iosMain
actual open class BaseViewModel {
override val scope: CoroutineScope
get() {
// these methods need to be defined
return this.getScopeInstance() ?: this.createScopeInstance()
}
// marks view model as cleared & closes the coroutine scope.
fun clear() {}
}
Not as straightforward. But also not complicated. When a scope is requested, we first check if one has already been created and return it, or we create a new one. We also don’t have an automatic way to bind to the lifecycle of a UIViewController so we’ll have to expose a method that should be called on deinit to clear the scope.
Using the ViewModel
Now that we’ve got a ViewModel we can write our business logic for both platforms!
class ExampleViewModel: ViewModel() {
private val _viewState = MutableStateFlow(UIViewState())
val viewState: StateFlow<UIViewState> = _viewState
fun onLaunched() {
scope.launch {
// fetch some data
_viewState.emit(newState)
}
}
}
Beautiful ✨
If you’re looking for the full implementation of the ViewModel, we’ve published the above as a small library on Github that you can use in your projects.
Have some thoughts? I’d love to hear them! 😊 Reach out to me on Twitter.
Know how this library can be improved? Send a Pull Request!