A Kotlin Multiplatform chat application that shares its entire UI and business logic across Android and iOS using Compose Multiplatform.
The app supports account registration and email verification, login and password reset, a chat list with an adaptive list/detail layout, one-to-one and group chats, chat management, and a user profile with avatar media picking.
Targets: Android (
androidMain) and iOS (iosArm64,iosSimulatorArm64). The convention plugins do not currently configure a Desktop/JVM target.
| Concern | Library |
|---|---|
| UI | Compose Multiplatform, Material 3, Compose Navigation, Adaptive layouts |
| Dependency injection | Koin |
| Networking | Ktor client (OkHttp on Android, Darwin on iOS) + WebSockets |
| Local persistence | Room (KMP) + SQLite, DataStore |
| Serialization | kotlinx.serialization |
| Async | kotlinx.coroutines |
| Images | Coil 3 |
| Logging | Kermit |
| Build config | BuildKonfig |
| Firebase | Firebase BOM (Android) |
Versions are centralized in the version catalog at gradle/libs.versions.toml.
The project is split into many small Gradle modules following a strict layered, per-feature structure. Module dependencies only point downward:
presentation → domain ← data → database
↑
core/* (shared by everything)
composeApp— shared application code and entry point (App.kt,NavigationRoot.kt). Aggregates every feature/core module and starts Koin viainitKoin().androidApp— the Android application module (Android manifest, launcher, Firebase / Google Services). Depends oncomposeApp.iosApp— the iOS application entry point (open in Xcode). Consumes the shared Kotlin code as a framework.core/core/domain— shared domain models and interfaces (no platform deps).core/data— shared data implementations and platform bindings.core/presentation— shared presentation utilities.core/designsystem— reusableChirp*Compose components.
feature/auth/—presentation,domain(login, register, email verification, forgot/reset password).feature/chat/—presentation,domain,data,database(chat list, chat detail, create/manage chat, profile).feature/chat/databaseis the Room KMP database; exported schemas live infeature/chat/database/schemas/.
Module build files are intentionally tiny because shared configuration lives in build-logic/convention/ as plugins applied by id:
convention.kmp.library— base KMP library (Android + iOS targets; namespace/framework name derived from the module path).convention.cmp.library— KMP library + Compose Multiplatform.convention.cmp.feature—cmp.library+ Koin, Compose navigation/viewmodel, andcore:presentation/core:designsystem. Use forfeature/*/presentation.convention.cmp.application— thecomposeAppitself.convention.android.application— theandroidAppmodule.convention.room— applies KSP + Room; registerskspAndroid/kspIosSimulatorArm64/kspIosArm64and sets the schema dir.convention.buildkonfig— generatesBuildKonfigwithAPI_KEY(package derived from the module path).
- MVI presentation — each screen folder contains
XxxViewModel,XxxState,XxxAction, and (when needed)XxxEvent. - Koin DI — one module per layer (
coreDataModule,chatDataModule,chatPresentationModule,corePresentationModule,authPresentationModule, …), all registered incomposeApp/.../di/initKoin.kt. Platform bindings useexpect/actualmodule declarations. - expect/actual — common code in
Platform.ktwithPlatform.android.kt/Platform.ios.ktactuals; the iOS data layer also uses a cinterop (network.def). - Offline-first repositories —
OfflineFirst*Repositoryread/write the Room cache and reconcile with Ktor/WebSocket services.
-
JDK 17+
-
Android Studio (latest) with the Kotlin Multiplatform tooling
-
Xcode (for building/running the iOS app)
-
API_KEYinlocal.properties—BuildKonfigConventionPluginreads it and fails the build if it is missing:API_KEY=your_api_key_here
./gradlew :composeApp:assembleDebug # build the Android APKOr use the run configuration from the run widget in the IDE toolbar.
Open the iosApp/ directory in Xcode and run, or use the IDE run configuration. The shared Kotlin code is exposed as a framework whose baseName is derived from the module path (e.g. :core:domain → CoreDomain).
./gradlew buildFor source sets targeting non-Android platforms, Room requires every @Query method to be either suspend or return an observable type (Flow). Always validate DAO changes with the iOS KSP task, not just the Android build:
./gradlew :feature:chat:database:kspKotlinIosSimulatorArm64Learn more about Kotlin Multiplatform and Compose Multiplatform.