Sharing View Models in Kotlin Multiplatform Mobile (iOS and Android)

Amadeusz Blanik
13 min readAug 11, 2023

Kotlin Multiplatform Mobile (KMM) stands out as a unique cross-platform technology, distinguishing itself from other solutions available in the market. Unlike many other frameworks, KMM enables the sharing of only the business logic between platforms, while the user interface remains fully native — a remarkable advantage. This approach typically involves forming three distinct teams to develop your application: iOS, Android, and shared developers. The iOS team concentrates on the native UI for the iOS platform, the Android team takes care of the Android UI, and the shared developers focus on crafting the business logic. This collaborative model facilitates efficient development, particularly when you strive to maximize code sharing. KMM simplifies sharing repositories, services, and databases, provided that your technology stack aligns with its principles.

One of the intriguing challenges KMM presents is the sharing of view models between iOS and Android, a task that brings increased complexity but offers substantial rewards.

The Case for Sharing View Models

I embarked on my journey with Kotlin Multiplatform Mobile while working on my side project, Wolfie.app, an innovative pet companion app designed for both iOS and Android platforms. When considering the best technology for my app, I weighed the merits of Flutter, React Native, and Kotlin Multiplatform Mobile. Opting for Flutter or React Native would have entailed compromising the native user experience — an outcome I was keen to avoid. My vision was to provide iOS and Android users with a seamlessly native experience. Additionally, taking the route of full native development would likely have consumed all my available time, given that I was a one-person team handling web, backend, and mobile development. This context made Kotlin Multiplatform Mobile the ideal choice.

As an iOS developer primarily, I found the initial steps in adopting KMM a bit steeper than they might have been for an Android developer. For those more inclined towards Android development, the checklist would be relatively shorter. Here are some key components to consider:

  • Kotlin: A fundamental requirement. Even if you have a background in iOS development, Kotlin is approachable and can be easily learned, even on your inaugural project.
  • Swift: An obvious necessity for iOS development. If you lack a Mac, acquiring one becomes imperative.
  • SwiftUI and Jetpack Compose: Declarative development paradigms that bring a new level of sophistication to UI design.
  • Coroutines: This concurrency framework plays a pivotal role, as most of your logic, repositories, and database operations will rely on it. If you possess experience with reactive programming, this will serve as a valuable foundation. Notably, coroutines also find application in iOS development.
  • KTOR: Your steadfast ally for any API connections.

Beyond these essentials, a plethora of libraries awaits your consideration. However, this article will specifically delve into the topic of sharing view models, exploring two noteworthy libraries and the do-it-yourself approach.

## moko-MVVM github

tl;dr
Licence: Apache 2.0
First public release was on Oct, 2019.
760 stars on GitHub
34 open issues
14 contributors

When it comes to sharing view models in Kotlin Multiplatform Mobile (KMM), the moko-MVVM library (GitHub) has garnered significant attention. Boasting an Apache 2.0 license, this library made its initial public release in October 2019. With an impressive count of 760 stars on GitHub, moko-MVVM showcases its popularity within the developer community. Backed by a team that has honed their focus on KMM for years, this library presents itself as a potent contender. However, while it holds great promise, there are some considerations to weigh before adoption.

A key advantage of moko-MVVM is its robust backing by a company committed to advancing the KMM ecosystem. Yet, an inherent trade-off exists, centered around the library’s alignment with declarative UI implementations. It’s important to note that moko-MVVM leans more towards UIKit rather than SwiftUI, potentially impacting its compatibility with the latest technologies. As of my writing, there appears to be no library compatible with Xcode 14.3.1, or at least the compatibility cannot be achieved seamlessly following the documentation. Given these circumstances, I’ve opted to defer my utilization of moko-MVVM for the time being.

Notably, moko-MVVM finds synergy with another noteworthy library from the same developer group: kswift (GitHub). This complementary library offers a wealth of Kotlin/Native API translations to Swift, enhancing cross-platform development. Additionally, it facilitates coroutine support on iOS, further streamlining your development workflow.

The promise held by moko-MVVM is undeniable, and its alignment with a company devoted to KMM adds to its credibility. However, for those heavily invested in SwiftUI and seeking seamless compatibility with the latest iOS technologies, careful consideration of its UIKit-centric approach is warranted. As the KMM landscape continues to evolve, the synergy between libraries like moko-MVVM and kswift could become increasingly compelling.

KMM-ViewModel github

tl;dr
Licence: MIT
First public release was on Dec, 2022.
353 stars on GitHub
4 open issues 2 contributors

In the realm of sharing view models across platforms with Kotlin Multiplatform Mobile (KMM), the KMM-ViewModel library (GitHub) emerges as a notable contender. With a MIT license and its initial public release in December 2022, KMM-ViewModel is a relatively new player in the field, yet it has already garnered attention with 353 stars on GitHub. While it’s in the alpha phase, its compatibility with SwiftUI right out of the box sets it apart. Moreover, its MIT licensing lends an advantageous flexibility in terms of potential maintenance efforts, particularly in the event of shifts in support.

Despite its current developmental stage, KMM-ViewModel offers a remarkable feature: seamless integration with SwiftUI, a key framework in modern iOS app development. This attribute could be particularly enticing for those who prioritize SwiftUI’s declarative and intuitive approach to user interface design. By providing a direct bridge between the KMM-ViewModel library and SwiftUI, developers can potentially achieve a harmonious synergy between platforms, promoting a consistent and native-like experience for users across iOS and Android.

It’s worth noting that KMM-ViewModel is an evolving library, still traversing its alpha phase. However, its alignment with SwiftUI underscores its potential to be a powerful tool for cross-platform development. Given its relatively recent entry into the landscape, it’s prudent to monitor its progression as it matures further.

An interesting facet of KMM-ViewModel lies in its reliance on the KMP-NativeCoroutines library (GitHub). This auxiliary library complements KMM-ViewModel by providing essential coroutine support on iOS — a critical component of modern mobile app development. Coroutines are pivotal for asynchronous programming and managing concurrency, and the inclusion of KMP-NativeCoroutines underscores the commitment to a robust development experience across platforms.

While KMM-ViewModel remains in alpha, its compatibility with SwiftUI and emphasis on coroutine support signal a potential breakthrough for seamless cross-platform view model sharing. As it continues to mature and gather feedback from the developer community, it could become a vital asset in the KMM toolkit.

Custom Solutions: Crafting Coroutines Support

In the pursuit of harmonizing coroutine support for both iOS and Android platforms within your Kotlin Multiplatform Mobile (KMM) project, custom solutions offer a versatile approach. By crafting shared functions and classes, you can bridge the gap and ensure seamless integration of asynchronous operations.

Let’s break down the custom solution step by step:

1. StateFlowClass: Shared State Flow

// shared/src/commonMain/kotlin/me/blanik/sample/Couritines.kt

package me.blanik.sample

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

class StateFlowClass<T>(private val delegate: StateFlow<T>) : StateFlow<T> by delegate {
fun subscribe(block: (T) -> Unit) = GlobalScope.launch(Dispatchers.IO) {
delegate.collect {
block(it)
}
}
}

fun <T> StateFlow<T>.asStateFlowClass(): StateFlowClass<T> = StateFlowClass(this)

This snippet establishes a StateFlowClass that wraps a StateFlow and offers a subscribefunction. It employs coroutines to bridge the gap between platforms, ensuring that data collection happens on the appropriate thread.

2. Shared Dispatchers

// shared/src/commonMain/kotlin/me/blanik/sample/Dispatchers.kt
package me.blanik.sample.database

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

expect val Dispatchers.IO: CoroutineDispatcher

In this section, shared dispatchers are set up for both platforms. On Android, we have a straightforward mapping to Android’s Dispatchers.IO. For iOS, we'll require more context:

3. Custom Coroutine Dispatcher for iOS

// shared/src/iosMain/kotlin/me/blanik/sample/Disaptchers.kt
package me.blanik.sample

import kotlinx.coroutines.*
import platform.darwin.*

actual val Dispatchers.IO: CoroutineDispatcher
get() = IODispatcher

@OptIn(InternalCoroutinesApi::class)
private object IODispatcher : CoroutineDispatcher(), Delay {
// Implementation details...
}

In this code block, we define a custom coroutine dispatcher for iOS. This dispatcher takes advantage of iOS’s dispatch mechanisms to ensure that coroutines run on the main queue and handle delays appropriately.

4. Flow Utilities

// shared/src/commonMain/kotlin/me/blanik/sample/FlowUtils.kt
package me.blanik.sample.database

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

class CFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
// Implementation details...
}

// Helper extension
internal fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this)

// Remove when Kotlin's Closeable is supported in K/N
interface Closeable {
fun close()
}

This snippet introduces CFlow, a class that enables the consumption of Flow-based APIs from Swift/Objective-C. It offers a clean way to handle subscriptions from these languages.

5. Platform-Specific Implementations

Android:

// shared/src/androidMain/kotlin/me/blanik/sample/Dispatchers.kt
package me.blanik.sample

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

actual val Dispatchers.IO: CoroutineDispatcher
get() = Dispatchers.IO

iOS:

// shared/src/iosMain/kotlin/me/blanik/sample/Disaptchers.kt
package me.blanik.sample

import kotlinx.coroutines.*
import platform.darwin.*

actual val Dispatchers.IO: CoroutineDispatcher
get() = IODispatcher

@OptIn(InternalCoroutinesApi::class)
private object IODispatcher : CoroutineDispatcher(), Delay {
// Implementation details...
}

Here, we finalize the setup by providing platform-specific implementations for dispatchers. For Android, we simply map to Android’s built-in dispatcher. For iOS, we delve into the custom dispatcher that bridges the gap between Kotlin coroutines and iOS’s dispatch mechanism.

With these components in place, you’ve created a comprehensive solution for managing coroutines and asynchronous operations within your KMM project. This approach empowers your project to handle concurrency seamlessly on both iOS and Android platforms, promoting consistent functionality and performance.

Implementing KMM-ViewModel: A Step-by-Step Guide

Incorporating KMM-ViewModel into your Kotlin Multiplatform Mobile (KMM) project opens up new avenues for seamless view model sharing across platforms. Let’s dive into the implementation process step by step:

  1. Upgrade Your Project to Kotlin 1.9:

In your build.gradle.kts file, update your Kotlin version to 1.9.0:

plugins {
id("com.android.application").version("8.1.0").apply(false)
id("com.android.library").version("8.1.0").apply(false)
kotlin("android").version("1.9.0").apply(false)
kotlin("multiplatform").version("1.9.0").apply(false)
}

2. Add Required Libraries to Your Shared Module:

Update your shared/build.gradle.kts file to include the necessary dependencies:

sourceSets {
val multiplatformSettingsVersion = "1.0.0"
val kmmViewModelVersion = "1.0.0-ALPHA-12"

all {
languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
}

val commonMain by getting {
dependencies {
implementation("com.russhwolf:multiplatform-settings-no-arg:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-serialization:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-coroutines:$multiplatformSettingsVersion")
implementation("com.rickclephas.kmm:kmm-viewmodel-core:$kmmViewModelVersion")
}
}
}

3. Sync Your Gradle Files:

Make sure to sync your Gradle files after adding the new dependencies.

4. Update Podfile for iOS:

In your iosApp directory, update the Podfile to include the required pods:

target 'iosApp' do
use_frameworks!
platform :ios, '14.1'
pod 'shared', :path => '../shared'
pod 'KMPNativeCoroutinesAsync', '1.0.0-ALPHA-13'
pod 'KMPNativeCoroutinesCombine', '1.0.0-ALPHA-13'
pod 'KMPNativeCoroutinesRxSwift', '1.0.0-ALPHA-13'
pod 'KMMViewModelSwiftUI', '1.0.0-ALPHA-12'
end

Run pod install in your iosApp directory.

5. Create Your First Shared View Model:

Create a new Kotlin file, SignInViewModel.kt, in your shared module:

package me.blanik.sample

import com.rickclephas.kmm.viewmodel.*
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
import kotlinx.coroutines.flow.*

open class SignInViewModel: KMMViewModel() {
private val _email = MutableStateFlow(viewModelScope, "")
private val _password = MutableStateFlow(viewModelScope, "")

@NativeCoroutinesState
val email = _email.asStateFlow()
@NativeCoroutinesState
val password = _password.asStateFlow()

fun setEmail(email: String) {
_email.value = email
}

fun setPassword(password: String) {
_password.value = password
}
}

6. Add iOS Implementation:

In your iosApp directory, create a Swift file named KMMViewModel.swift:

import KMMViewModelCore
import shared

extension Kmm_viewmodel_coreKMMViewModel: KMMViewModel { }

Also you need to update your ContentView.swift file:

import SwiftUI
import KMMViewModelSwiftUI
import shared

extension ContentView {
class ViewModel: shared.SignInViewModel {}
}

struct ContentView: View {
@StateViewModel var viewModel = ViewModel()

var body: some View {
VStack {
List {
Section(header: Text("Input")) {
HStack {
Text("Email")

TextField("Email here", text: Binding(get: {
viewModel.email
}, set: { value in
viewModel.setEmail(email: value)
}))
}

HStack {
Text("Password")

SecureField("Type here", text: Binding(get: {
viewModel.password
}, set: { value in
viewModel.setPassword(password: value)
}))
}
}

Section(header: Text("Output")) {
HStack {
Text("Email")

Text(viewModel.email)
}
HStack {
Text("Password")

Text(viewModel.password)
}
}
}
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Binding kotlin couritines to SwiftUI TextField components:

Couritines is one way communication. You need to attach them as the @State (or @Published) values which two-way communication and natively supported by iOS. To archive that you can use create custom binding for getter and setter.

Binding(get: {

}, set: { value in
viewModel.…(…)
})

6. Add Android Implementation:

In your Android module, create a Composable function for the sign-in screen:

@Composable
fun SignInScreen(
signInViewModel: SignInViewModel = SignInViewModel()
) {
val emailState by signInViewModel.email.collectAsState()
val passwordState by signInViewModel.password.collectAsState()

Column {
Text("Input")
OutlinedTextField(
label = { Text(text = "Email") },
value = emailState,
onValueChange = signInViewModel::setEmail
)
OutlinedTextField(
label = { Text(text = "Password") },
value = passwordState,
onValueChange = signInViewModel::setPassword,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
Text("Output")
Text(text = emailState)
Text(text = passwordState)
}
}

In your Android MainActivity, use the SignInScreen Composable function:

class MainActivity : ComponentActivity() {
private val viewModel: SignInViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Column {
SignInScreen(viewModel)
}
}
}
}
}
}

With these steps, you’ve successfully integrated KMM-ViewModel into your project, enabling seamless view model sharing between iOS and Android platforms. This comprehensive guide should help you navigate through the implementation process and leverage the power of shared view models in your Kotlin Multiplatform Mobile development journey.

Extra — API Connection in KMM

Adding API connectivity to your Kotlin Multiplatform Mobile (KMM) project through Ktor can greatly enhance your app’s functionality. Let’s go through the steps of integrating API calls into your existing KMM project:

  1. Update Shared Build Gradle Configuration:

In your shared/build.gradle.kts file, add Ktor dependencies to your common source set:

// shared/build.gradle.kts
kotlin {
// ...

sourceSets {
val ktorVersion = "2.3.3"

val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-client-serialization:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-auth:$ktorVersion")
}
}

// ...
}
}

2. Create API Models:

Define your API response models and payloads. For example, create ApiAuthSignInResponse and ApiAuthSignInPayload classes in your shared module:

// shared/src/commonMain/kotlin/me/blanik/sample/network/ApiAuthSignIn.kt
package me.blanik.sample.network

import kotlinx.serialization.Serializable

@Serializable
data class ApiAuthSignInResponse(
val accessToken: String,
val refreshToken: String? = null
)

@Serializable
data class ApiAuthSignInPayload(
val username: String,
val password: String,
val keepSignIn: Boolean? = null,
val device: String? = null
)

3. Create API Response Handling:

Define a class to represent API error responses and responses containing data. For example:

// shared/src/commonMain/kotlin/me/blanik/sample/network/ApiResponse.kt
package me.blanik.sample.network

import io.ktor.util.date.GMTDate
import kotlinx.serialization.Serializable

@Serializable
data class ApiError(
val statusCode: Int = 400,
val message: String = "",
val timestamp: String = GMTDate().toString(),
val errors: List<ApiErrorErrors>? = null
)

@Serializable
data class ApiErrorErrors(
val property: String = "",
val children: List<ApiErrorErrors> = emptyList(),
val constraints: Map<String, String> = emptyMap()
)

class ApiResponse<T>(success: T?, error: ApiError?) {
var success: T? = success
var failure: ApiError? = error
}

4. Create API Service:

Implement your API service using Ktor’s HTTP client. Define a class that encapsulates API endpoints and handles API requests. For example:

// shared/src/commonMain/kotlin/me/blanik/sample/network/WolfieApi.kt
package me.blanik.sample.network

import io.ktor.client.*
import io.ktor.client.call.receive
import io.ktor.client.request.*
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.contentType
import io.ktor.http.isSuccess
import io.ktor.serialization.Serializable
import kotlinx.serialization.json.Json

class WolfieApi {
private val client by lazy {
HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(Json {
ignoreUnknownKeys = true
prettyPrint = true
})
}
}
}

suspend fun authSignIn(payload: ApiAuthSignInPayload): ApiResponse<ApiAuthSignInResponse> {
val response = client.post<HttpResponse>("$API_BASE_URL$AUTH_SIGN_IN") {
contentType(ContentType.Application.Json)
body = payload
}

return if (response.status.isSuccess()) {
ApiResponse(response.receive(), null)
} else {
ApiResponse(null, response.receive())
}
}

companion object {
private const val API_BASE_URL = "https://api-x.wolfie.app/v2"
private const val AUTH_SIGN_IN = "/auth/sign-in"
}
}

This class defines a function authSignIn that sends a POST request to the specified API endpoint, using the provided payload. It returns an ApiResponse containing either a successful response or an error response.

5. Use API in Shared View Model:

Integrate the API service into your shared view model. Use the WolfieApi class to make API calls, handle responses, and update the view model's state accordingly. For instance:

// shared/src/commonMain/kotlin/me/blanik/sample/SignInViewModel.kt
import me.blanik.sample.network.ApiAuthSignInPayload
import me.blanik.sample.network.WolfieApi

enum class ApiState {
INIT,
PENDING,
SUCCESS,
FAILURE
}

open class SignInViewModel : KMMViewModel() {
private val wolfieApi = WolfieApi()

// ...

@NativeCoroutinesState
val state = _state.asStateFlow()

@NativeCoroutinesState
val errorMessage = _errorMessage.asStateFlow()

// ...

suspend fun signIn() {
_state.value = ApiState.PENDING

val response = wolfieApi.authSignIn(
ApiAuthSignInPayload(
username = _email.value,
password = _password.value
)
)

if (response.success != null) {
_state.value = ApiState.SUCCESS
} else {
_state.value = ApiState.FAILURE
_errorMessage.value = response.failure?.message ?: null
}
}
}

6. Update iOS Implementation:

In your iOS implementation (Swift), you can display the API state and error message:

// iosApp/iosApp/ContentView.swift
// ...
struct ContentView: View {
// ...

var body: some View {
VStack {
// ...
Section(header: Text("Output")) {
// ...
HStack {
Text("State")
Text(viewModel.state.rawValue)
}
HStack {
Text("Error message")
Text(viewModel.errorMessage ?? "—")
}
}
// ...
Section(header: Text("Action")) {
Button("Sign in") {
Task {
try await viewModel.signIn()
}
}
}
}
}
}

7. Update Android Implementation:

In your Android implementation, you can also display the API state and error message:

// androidApp/src/main/java/me/blanik/sample/android/MainActivity.kt
// ...
@Composable
fun SignInScreen(
signInViewModel: SignInViewModel = SignInViewModel()
) {
// ...
val stateState by signInViewModel.state.collectAsState()
val errorMessage by signInViewModel.errorMessage.collectAsState()

Column {
// ...
Text("Output")
Text(text = stateState.name)
Text(text = errorMessage ?: "—")
// ...
Button(onClick = {
GlobalScope.async(Dispatchers.Main) {
signInViewModel.signIn()
}
}) {
Text(text = "Sign in")
}
}
}

With these steps, you’ve successfully integrated API connectivity into your KMM project using Ktor. You can now make API calls from shared view models and handle responses across both iOS and Android platforms.

Summary

Kotlin Multiplatform Mobile (KMM) is rapidly advancing and has become a viable option for production applications. The community is growing, and various libraries are emerging to address different use cases, each with its own set of advantages and considerations. When adopting KMM, it’s important to carefully evaluate your project’s needs and choose the right solutions.

In this guide, we’ve explored how to share view models between iOS and Android applications using the KMM-ViewModel library. By creating shared view models and using the KMM-ViewModel library, you can ensure that your business logic is consistent across platforms while allowing native developers to focus on UI development. Keep in mind that shared module developers need to be well-versed in both platforms to effectively support and maintain the shared codebase.

You can find the code samples and implementation details covered in this guide on GitHub. If you have any further questions or need assistance, feel free to comment or contact me via LinkedIn.

KMM offers a promising approach to building cross-platform applications, and as the ecosystem continues to evolve, it’s an exciting space to be a part of.

--

--

Amadeusz Blanik
0 Followers

Tech Lead of Frontend & Mobile Development