Kotlin Multiplatform Mobile (KMM) lets you share business logic across Android and iOS while keeping platform UIs native. The strategy: share domain models, repository interfaces, and use cases; leave ViewModel and UI platform-specific.
What to Share
Shared (commonMain):
├── domain/
│ ├── model/ Article.kt, User.kt
│ ├── repository/ ArticleRepository.kt (interface)
│ └── usecase/ GetArticlesUseCase.kt
├── data/
│ ├── remote/ ArticleApi.kt (Ktor)
│ ├── cache/ ArticleCache.kt (SQLDelight)
│ └── repository/ ArticleRepositoryImpl.kt
└── util/
└── DateFormatter.kt
Android-specific (androidMain):
├── DI (Hilt modules)
├── ViewModel
└── Compose UI
iOS-specific (iosMain):
├── DI (manual or Koin)
├── ObservableObject wrappers
└── SwiftUI
Shared Domain Layer
// commonMain/domain/model/Article.kt
data class Article(
val id: String,
val title: String,
val body: String,
val publishedAt: Long,
val authorName: String
)
// commonMain/domain/repository/ArticleRepository.kt
interface ArticleRepository {
fun observeArticles(): Flow<List<Article>>
suspend fun refreshArticles()
suspend fun getArticle(id: String): Article?
}
// commonMain/domain/usecase/GetArticlesUseCase.kt
class GetArticlesUseCase(private val repository: ArticleRepository) {
operator fun invoke(): Flow<List<Article>> = repository.observeArticles()
.map { articles -> articles.sortedByDescending { it.publishedAt } }
}
Shared Data Layer (Ktor + SQLDelight)
// commonMain/data/remote/ArticleApi.kt
class ArticleApi(private val client: HttpClient) {
suspend fun fetchArticles(): List<ArticleDto> {
return client.get("https://api.example.com/articles").body()
}
}
// Ktor HttpClient factory (configured per platform in expect/actual)
// commonMain expects a factory; androidMain/iosMain provides platform engine
// commonMain/data/repository/ArticleRepositoryImpl.kt
class ArticleRepositoryImpl(
private val api: ArticleApi,
private val queries: ArticleQueries // SQLDelight generated
) : ArticleRepository {
override fun observeArticles(): Flow<List<Article>> {
return queries.selectAll()
.asFlow()
.mapToList(Dispatchers.Default)
.map { it.map { row -> row.toDomain() } }
}
override suspend fun refreshArticles() {
val dtos = api.fetchArticles()
queries.transaction {
dtos.forEach { dto ->
queries.upsert(
id = dto.id,
title = dto.title,
body = dto.body,
published_at = dto.publishedAt,
author_name = dto.authorName
)
}
}
}
}
Platform-Specific DI
Android (Hilt module)
@Module
@InstallIn(SingletonComponent::class)
object SharedModule {
@Provides @Singleton
fun provideHttpClient(): HttpClient = HttpClient(OkHttp) {
install(ContentNegotiation) { json() }
install(Logging) { level = LogLevel.INFO }
}
@Provides @Singleton
fun provideArticleApi(client: HttpClient) = ArticleApi(client)
@Provides @Singleton
fun provideArticleRepository(
api: ArticleApi,
database: AppDatabase
): ArticleRepository = ArticleRepositoryImpl(api, database.articleQueries)
@Provides
fun provideGetArticlesUseCase(repository: ArticleRepository) =
GetArticlesUseCase(repository)
}
iOS (Koin or Manual)
// SwiftUI — wrap Flow as @Published using Kotlin coroutines/concurrency bridge
class ArticlesObservable: ObservableObject {
@Published var articles: [Article] = []
private let useCase: GetArticlesUseCase
init(useCase: GetArticlesUseCase) {
self.useCase = useCase
observeArticles()
}
private func observeArticles() {
// Use KMM's FlowCollector bridge
useCase.invoke().collect { articles in
DispatchQueue.main.async {
self.articles = articles as! [Article]
}
}
}
}
expect/actual for Platform APIs
// commonMain
expect fun currentTimeMillis(): Long
expect fun formatDate(timestamp: Long): String
// androidMain
actual fun currentTimeMillis(): Long = System.currentTimeMillis()
actual fun formatDate(timestamp: Long): String =
SimpleDateFormat("MMM d, yyyy", Locale.getDefault()).format(Date(timestamp))
// iosMain
actual fun currentTimeMillis(): Long = Clock.System.now().toEpochMilliseconds()
actual fun formatDate(timestamp: Long): String =
NSDateFormatter().also {
it.dateFormat = "MMM d, yyyy"
}.stringFromDate(NSDate(timeIntervalSince1970 = timestamp / 1000.0))
Key Takeaways
| Layer | Shared? | Why |
|---|---|---|
| Domain models | Yes | Pure data; no platform dependency |
| Repository interfaces | Yes | Define contract; impl uses platform-neutral libraries |
| Use cases | Yes | Pure business logic with coroutines |
| Network (Ktor) | Yes | Multiplatform HTTP client |
| Database (SQLDelight) | Yes | Generates type-safe Kotlin from SQL |
| ViewModel | No | Android-specific lifecycle; iOS uses ObservableObject |
| UI | No | Compose (Android) / SwiftUI (iOS) |