← All courses

Shared ViewModels & Architecture

🗓 May 31, 2026 ⏱ 2 min read

The recommended architecture

A clean KMP app layers like this, with the line for “shared” drawn just below the UI:

Compose UI (Android) SwiftUI (iOS) Shared ViewModel (StateFlow of UiState) Repository -> Ktor + SQLDelight

A shared ViewModel

You can put presentation logic in commonMain and expose the screen as a single StateFlow of a sealed UI state — the same pattern you learned in Kotlin and Compose, now shared.

sealed interface UsersUiState {
    object Loading : UsersUiState
    data class Success(val users: List<User>) : UsersUiState
    data class Error(val message: String) : UsersUiState
}

class UsersViewModel(private val repo: UserRepository) {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

    private val _state = MutableStateFlow<UsersUiState>(UsersUiState.Loading)
    val state: StateFlow<UsersUiState> = _state

    fun load() {
        scope.launch {
            _state.value = UsersUiState.Loading
            _state.value = runCatching { repo.getUsers() }
                .fold(
                    onSuccess = { UsersUiState.Success(it) },
                    onFailure = { UsersUiState.Error(it.message ?: "Error") }
                )
        }
    }

    fun clear() { scope.cancel() }   // call when the screen goes away
}

How each platform consumes it

  • Android — collect state as Compose state and render with a when.
  • iOS — observe state from Swift (via SKIE or a small wrapper) and drive a SwiftUI view.

The decision “loading vs success vs error” is made once in shared code; both UIs simply display whatever state they’re given.

Tools that make this nicer

  • SKIE — native-feeling Flow/suspend in Swift.
  • Koin — multiplatform dependency injection to wire repositories into ViewModels.
  • SQLDelight — a shared, type-safe database.
  • moko-mvvm / KMP-ObservableViewModel — helpers for sharing ViewModels with lifecycle support.

Common mistakes

  • Putting platform UI logic into the shared ViewModel.
  • Forgetting to cancel the ViewModel’s scope, leaking coroutines.
  • Trying to share the UI before the logic layer is solid — share logic first.
Summary: Share ViewModels in common code that expose a single StateFlow of a sealed UI state; let Compose (Android) and SwiftUI (iOS) render it natively. Use Koin, SQLDelight and SKIE to make the shared layer clean and ergonomic.