From cc-mobile-kmm
How MVVM + Clean Architecture is applied in this Kotlin Multiplatform codebase — source-set placement, `expect`/`actual` vs. interface injection, layer boundaries inside `commonMain`, and how ViewModels reach both Android and iOS. Load when designing a feature, deciding where code belongs, or reviewing layer boundaries in the shared module.
How this skill is triggered — by the user, by Claude, or both
Slash command
/cc-mobile-kmm:kmm-architectureThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
```
commonMainpresentation — ViewModels, UiState/UiEvent/UiAction. (Runs on both platforms)
↓
domain — Pure Kotlin. Models, repository interfaces, use cases.
↑
data — Repository implementations. Ktor clients, SQLDelight DAOs, mappers.
Dependency rule: presentation → domain ← data. Same as the native Android and iOS setups. The domain layer imports only Foundation-equivalent Kotlin + coroutines + kotlinx-* (serialization, datetime).
When adding code, ask:
kotlinx-* dependencies? → commonMain.androidMain.Foundation, UIKit, Security for Keychain)? → iosMain.HttpClient engine)? → expect/actual.KeyValueStore, a CryptoProvider)? → interface in commonMain, implementations in platform source sets, bound via Koin.Prefer the higher-numbered options only when the lower ones don't fit. commonMain-first.
expect / actual vs. interface + KoinUse expect / actual when:
// commonMain
internal expect fun platformHttpEngine(): HttpClientEngine
// androidMain
internal actual fun platformHttpEngine(): HttpClientEngine = OkHttp.create()
// iosMain
internal actual fun platformHttpEngine(): HttpClientEngine = Darwin.create()
Use an interface + Koin when:
KeyValueStore with in-memory, file-backed, and encrypted variants).// commonMain/domain
interface KeyValueStore {
suspend fun getString(key: String): String?
suspend fun putString(key: String, value: String)
suspend fun remove(key: String)
}
// androidMain
class DataStoreKeyValueStore(private val dataStore: DataStore<Preferences>) : KeyValueStore { ... }
// iosMain
class NSUserDefaultsKeyValueStore : KeyValueStore { ... }
| Layer | commonMain | androidMain | iosMain |
|---|---|---|---|
| Domain models | ✓ | ||
| Use cases | ✓ | ||
| Repository interfaces | ✓ | ||
| Repository impls | ✓ (when using HttpClient + SQLDelight) | Android-specific impls only | iOS-specific impls only |
DTOs (@Serializable) | ✓ | ||
| Mappers | ✓ | ||
Ktor HttpClient config | ✓ | Platform engine (OkHttp) | Platform engine (Darwin) |
| SQLDelight schema | ✓ | SqlDriver (Android driver) | SqlDriver (Native driver) |
| ViewModels | ✓ | ||
| Koin modules | core modules common; platform-specific as needed | androidModule (binds Context, SharedPreferences, etc.) | iosModule (binds Darwin-specific impls) |
| Navigation | ✓ (Navigation-Compose) | ✓ (NavigationStack in Swift) | |
| UI | ✓ (Jetpack Compose) | ✓ (SwiftUI) |
orders)shared/src/commonMain/kotlin/com/example/app/feature/orders/
├── domain/
│ ├── model/Order.kt
│ ├── model/OrderId.kt # value class
│ ├── repository/OrderRepository.kt
│ └── usecase/GetOrderUseCase.kt
├── data/
│ ├── remote/OrderApi.kt
│ ├── remote/OrderDto.kt
│ ├── mapper/OrderMapper.kt
│ ├── repository/OrderRepositoryImpl.kt
│ └── di/orderDataModule.kt
└── presentation/orders/
├── OrderUiState.kt
├── OrderUiEvent.kt
├── OrderAction.kt
└── OrderViewModel.kt
shared/src/commonTest/kotlin/com/example/app/feature/orders/
├── domain/usecase/GetOrderUseCaseTest.kt
├── data/mapper/OrderMapperTest.kt
├── data/repository/OrderRepositoryImplTest.kt # uses Ktor MockEngine
└── presentation/OrderViewModelTest.kt
A shared ViewModel extends androidx.lifecycle.ViewModel (multiplatform-ready):
class OrderViewModel(
private val getOrder: GetOrderUseCase,
) : ViewModel() {
private val _state = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val state: StateFlow<OrderUiState> = _state.asStateFlow()
private val _events = Channel<OrderUiEvent>(Channel.BUFFERED)
val events: Flow<OrderUiEvent> = _events.receiveAsFlow()
fun onAction(action: OrderAction) { ... }
}
state in a Composable via viewModel.state.collectAsStateWithLifecycle() and sends actions with viewModel::onAction.@Observable Swift class that subscribes to state and exposes Swift-friendly properties. See kmm-ios-interop skill.commonMain/.../domain/model/commonMain/.../domain/repository/commonMain/.../domain/usecase/ (if warranted)commonMain/.../data/commonMain/.../data/remote/commonMain/.../data/repository/commonMain/.../data/di/, registered at the rootcommonMain/.../presentation/<feature>/commonMain/.../presentation/<feature>/commonTest/ for use cases, mappers, repository (with MockEngine), ViewModel:shared (in ../android/)../ios/)npx claudepluginhub dimitriremoiville/cc-mobile --plugin cc-mobile-kmmSearches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.