Skip to Content
David Chavez

Kotlin Multiplatform Tales: A Shared ViewModel

Published on Apr 7, 2022 · Kotlin Multiplatform

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:

  1. On Android our ViewModel should also be able to leverage lifecycle awareness.
  2. 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!