Multiplatform ViewModel
The Android ViewModel allows you to connect the business logic of your app with the UI components. With Compose Multiplatform, you can also use ViewModels in common code.
This page walks you through setting up and working with ViewModels in a multiplatform project:
Choose how much of your ViewModel and UI code to share: from a fully shared approach to sharing only the repository or data layer.
Set up dependencies
To share ViewModels and UI across platforms:
Define the dependencies in a Gradle version catalog file:
[versions] androidx-viewmodel = "2.10.0" [libraries] androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-viewmodel" } androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-viewmodel" }In the
build.gradle.ktsfile of the KMP module, add the following dependencies to thecommonMainsource set:kotlin { // ... sourceSets { // ... commonMain.dependencies { implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.navigation3) } // ... } }
If you have a desktop target, add the kotlinx-coroutines-swing dependency as well. When running coroutines in a ViewModel, ViewModel.viewModelScope is tied to Dispatchers.Main.immediate, which might be unavailable on desktop by default. The Kotlinx Coroutines Swing library makes ViewModel coroutines work correctly with Compose Multiplatform.
In the Gradle version catalog:
[versions] kotlinx-coroutines = "1.10.2" [libraries] kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }In the
build.gradle.ktsfile:kotlin { // ... sourceSets { // ... jvmMain.dependencies { implementation(libs.kotlinx.coroutines.swing) } // ... } }See the
Dispatchers.Maindocumentation for details.
Using ViewModel in common code
Compose Multiplatform provides a common ViewModelStoreOwner implementation, so using the ViewModel class in common code is not much different from Android best practices.
However, there is an important difference on non-JVM platforms, where type reflection for instantiating objects is not available. You cannot call the viewModel() function without parameters in common code. Every time you create a ViewModel instance, you need to provide at least an initializer as an argument.
If only an initializer is provided, Compose Multiplatform creates a default factory under the hood. However, you can implement your own factories and call more explicit versions of the common viewModel() function, just like with Jetpack Compose.
Let's define a ViewModel and wire it into a composable:
Define a simple
OrderViewModelclass that manages the UI state, including the quantity and price of the ordered item:data class OrderUiState(val quantity: Int = 0, val price: String = "$0.00") class OrderViewModel : ViewModel() { val uiState: StateFlow<OrderUiState> field = MutableStateFlow(OrderUiState()) fun setQuantity(n: Int) { field.update { it.copy(quantity = n, price = "$${n * 2}.00") } } }Add the custom ViewModel to your composable function using the common
viewModel()function with an initializer:import com.example.ui.OrderViewModel @Composable fun CupcakeApp( viewModel: OrderViewModel = viewModel { OrderViewModel() }, ) { // ... }
ViewModel scoping with Navigation 3
When using ViewModels with Navigation 3 in common code, ViewModels are not automatically scoped to navigation entries by default. Without explicit scoping, each ViewModel will be tied to the Activity rather than the screen, even after the user has navigated away.
To scope ViewModels and saveable Compose state per navigation entry, pass the Navigation 3 entry decorators to NavDisplay when defining the navigation destinations:
ViewModel and dependency injection
A dependency injection (DI) framework allows you to inject different dependencies into components based on the current environment or target platform. To manage ViewModels, you can use Koin, Metro, or any other DI framework that supports Kotlin Multiplatform.
For an advanced example of dependency injection usage, see the Share data access layer tutorial.
Koin
Koin is a runtime DI framework that provides either a DSL or annotations for configuring your dependencies. To use Koin with Compose ViewModels, add the koin-compose-viewmodel dependency.
You can then inject a ViewModel into a Composable function using koinViewModel():
For details, see the Koin documentation on ViewModel support and injecting ViewModels in Compose.
Metro
Metro is a compile-time DI framework implemented as a Kotlin compiler plugin. To use Metro with Compose ViewModels, add the metrox-viewmodel-compose dependency.
Then you can inject a ViewModel into a Composable function using metroViewModel():
For details, see the MetroX documentation on ViewModel integration and accessing ViewModels in Compose.
Levels of code sharing
You can choose which parts of your code to share and which to keep platform-specific:
To share both UI and business logic between platforms, see the shared logic and UI tutorial.
To share some code without sharing UI implementation, see the shared logic tutorial.
The following examples show how to use ViewModel at different levels of code sharing. All examples are based on the OrderViewModel class introduced above.
Shared ViewModel and UI
In this approach, everything, including the ViewModel and the UI, is shared via Compose Multiplatform. You write your app's UI code once, and it will work on all platforms.
Shared ViewModel and platform-specific UI
In this approach, the ViewModel (business logic) is shared, but platforms have native UI implementations. Learn more in Set up ViewModel for Kotlin Multiplatform.
Since the UI is not shared in this case, you can switch from the Compose Multiplatform version of the ViewModel library to the androidx.lifecycle library.
Update the dependencies in the Gradle version catalog:
[versions] androidx-viewmodel = "2.10.0" [libraries] androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-viewmodel" }In the
build.gradle.ktsfile, declare the dependency asapi, as it needs to be exported to the binary framework:kotlin { // ... sourceSets { // ... commonMain.dependencies { api(libs.androidx.lifecycle.viewmodel) } // ... } }
Android implementation
On Android, Jetpack Compose automatically finds the ViewModelStoreOwner provided by the Activity and supplies the OrderViewModel.
iOS implementation
On iOS, there is no built-in ViewModelStoreOwner, so the ViewModel's lifecycle must be tied to SwiftUI manually. We recommend using the KMP-ObservableViewModel library, which lets SwiftUI observe Kotlin Multiplatform ViewModels directly and handles the required ViewModel lifecycle/store-owner boilerplate for iOS.
Export ViewModel APIs for access from Swift:
listOf( iosArm64(), iosSimulatorArm64(), ).forEach { it.binaries.framework { export(libs.androidx.lifecycle.viewmodel) baseName = "shared" } }Define your ViewModel in
commonMainusing KMP-ObservableViewModel's ViewModel base class and the@NativeCoroutinesStateannotation:import com.rickclephas.kmp.observableviewmodel.ViewModel import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class OrderViewModel : ViewModel() { private val _uiState = MutableStateFlow(OrderUiState()) @NativeCoroutinesState val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow() fun setQuantity(n: Int) { _uiState.value = _uiState.value.copy(quantity = n) } }Use the ViewModel in the iOS UI entry point:
import SwiftUI import shared import KMPObservableViewModelSwiftUI @main struct iOSCupcakeApp: App { var body: some Scene { WindowGroup { CupcakeView() } } } struct CupcakeView: View { @StateViewModel private var viewModel = OrderViewModel() var body: some View { VStack { Text("Quantity: \(viewModel.uiState.quantity)") Text("Price: \(viewModel.uiState.price)") Button("Set Quantity to '6'") { viewModel.setQuantity(n: 6) } } } }
Shared repo/data layer, platform-specific ViewModels and UI
Another option is to only share the data and repository layer while using platform-specific ViewModel implementations. This allows you to use native patterns on each platform, such as Hilt for Android dependency injection, or ObservableObject with Combine for iOS.
Create a shared repository class with the data logic:
class OrderRepository { fun calculatePrice(quantity: Int) = "$${quantity * 2}.00" }Implement platform-specific ViewModels.
On Android, use the standard Android ViewModel and inject the repository:
class AndroidOrderViewModel( private val repo: OrderRepository ) : ViewModel() { val uiState: StateFlow<OrderUiState> field = MutableStateFlow(OrderUiState()) fun setQuantity(n: Int) { uiState.update { it.copy(quantity = n, price = repo.calculatePrice(n)) } } }On iOS, implement the ViewModel natively in Swift using
ObservableObject:import shared class IOSOrderViewModel: ObservableObject { private let repo: OrderRepository @Published var uiState: OrderUiState = OrderUiState() init(repo: OrderRepository) { self.repo = repo } func setQuantity(n: Int32) { uiState = OrderUiState(quantity: n, price: repo.calculatePrice(quantity: n)) } }
Implement platform-specific UI.
On Android:
@Composable fun AndroidCupcakeApp( viewModel: AndroidOrderViewModel = viewModel { AndroidOrderViewModel(OrderRepository()) } ) { val uiState by viewModel.uiState.collectAsState() Column { Text("Quantity: ${uiState.quantity}") Text("Price: ${uiState.price}") Button(onClick = { viewModel.setQuantity(6) }) { Text("Set Quantity to '6'") } } }On iOS:
struct IOSCupcakeApp: App { @StateObject var viewModel = IOSOrderViewModel(repo: OrderRepository()) var body: some View { VStack { Text("Quantity: \(viewModel.uiState.quantity)") Text("Price: \(viewModel.uiState.price)") Button("Set Quantity to '6'") { viewModel.setQuantity(n: 6) } } } }
What's next
Check out the full sample.
See Set up ViewModel for Kotlin Multiplatform for additional Android-focused guidance.
Learn how to integrate Compose Multiplatform with SwiftUI when using shared ViewModels with native UI.