A production-ready Compose Multiplatform template with modern Android/iOS architecture, featuring Navigation 3, Room Database, DataStore, Koin DI, and Ktor networking.
| Library | Version | Purpose |
|---|---|---|
| Compose Multiplatform | 1.10.0-beta02 | UI Framework |
| Kotlin | 2.2.21 | Language |
| Navigation 3 | 1.0.0-alpha05 | Type-safe Navigation |
| Room | 2.8.4 | Local Database |
| DataStore | 1.2.0 | Preferences Storage |
| Koin | 4.0.4 | Dependency Injection |
| Ktor | 3.1.3 | Networking |
| Kotlinx Serialization | 1.9.0 | JSON Parsing |
| Kotlinx Coroutines | 1.10.2 | Async Operations |
composeApp/src/
βββ commonMain/kotlin/dev/serializer/template/cmp/
β βββ data/
β β βββ local/
β β β βββ dao/UserDao.kt
β β β βββ entity/UserEntity.kt
β β β βββ AppDatabase.kt
β β β βββ DatabaseBuilder.kt (expect)
β β βββ preferences/
β β β βββ PreferencesDataStore.kt
β β βββ remote/
β β β βββ model/PostDto.kt
β β β βββ ApiService.kt
β β β βββ HttpClientFactory.kt
β β β βββ NetworkClient.kt
β β βββ repository/
β β βββ UserRepository.kt
β βββ di/
β β βββ AppModule.kt
β βββ domain/
β β βββ model/User.kt
β βββ navigation/
β β βββ Routes.kt
β β βββ AppNavigation.kt
β βββ presentation/
β β βββ screens/
β β β βββ HomeScreen.kt
β β β βββ DetailScreen.kt
β β β βββ SettingsScreen.kt
β β βββ viewmodel/
β β βββ HomeViewModel.kt
β β βββ SettingsViewModel.kt
β βββ util/
β β βββ Result.kt
β β βββ TimeUtils.kt
β βββ App.kt
βββ androidMain/kotlin/...
β βββ CMPApplication.kt
β βββ MainActivity.kt
β βββ DatabaseBuilder.android.kt (actual)
β βββ PreferencesDataStore.android.kt (actual)
β βββ HttpClientFactory.android.kt (actual)
β βββ TimeUtils.android.kt (actual)
βββ iosMain/kotlin/...
βββ MainViewController.kt
βββ DatabaseBuilder.ios.kt (actual)
βββ PreferencesDataStore.ios.kt (actual)
βββ HttpClientFactory.ios.kt (actual)
βββ TimeUtils.ios.kt (actual)
Type-safe navigation with serializable routes:
// Define routes
@Serializable
sealed interface Route {
@Serializable
data object Home : Route
@Serializable
data class Detail(val id: String) : Route
}
// Navigate
backStack.add(Route.Detail(id = "123"))Full Room support for both Android and iOS:
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val email: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<UserEntity>>
@Insert
suspend fun insertUser(user: UserEntity): Long
}Type-safe networking with automatic error handling:
// Simple API calls
class ApiService(private val networkClient: NetworkClient) {
suspend fun getPosts(): Result<List<PostDto>> =
networkClient.get("/posts")
suspend fun createPost(post: PostDto): Result<PostDto> =
networkClient.post("/posts", post)
}
// Handle results elegantly
apiService.getPosts()
.onSuccess { posts -> /* handle success */ }
.onError { exception -> /* handle error */ }
// Or transform data
apiService.getPosts()
.map { posts -> posts.filter { it.userId == 1 } }
.getOrNull()Comprehensive error handling:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: AppException) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
// Exception types
sealed class AppException {
data class NetworkException(...) // Connection issues
data class ServerException(...) // 5xx errors
data class ClientException(...) // 4xx errors
data class SerializationException(...) // JSON parsing
data class TimeoutException(...) // Request timeout
data class UnknownException(...) // Other errors
}Cross-platform preferences storage:
class PreferencesRepository(private val dataStore: DataStore<Preferences>) {
val darkModeFlow: Flow<Boolean> = dataStore.data.map {
it[PreferencesKeys.DARK_MODE] ?: false
}
suspend fun setDarkMode(enabled: Boolean) {
dataStore.edit { it[PreferencesKeys.DARK_MODE] = enabled }
}
}Simple and powerful DI:
val appModule = module {
// Database
single { getDatabaseBuilder().build() }
single { get<AppDatabase>().userDao() }
// Network
single { createHttpClient() }
single { NetworkClient(get(), ApiConfig.BASE_URL) }
singleOf(::ApiService)
// ViewModels
viewModelOf(::HomeViewModel)
}- Android Studio Ladybug or later
- Xcode 15+ (for iOS)
- JDK 17+
Android:
./gradlew :composeApp:assembleDebugiOS:
./gradlew :composeApp:compileKotlinIosSimulatorArm64
# Then open iosApp/iosApp.xcodeproj in XcodeUpdate the base URL in AppModule.kt:
object ApiConfig {
const val BASE_URL = "https://your-api.com"
}Important: The sample app includes dummy data for demonstration purposes only:
- Users: Stored locally in Room database - you can create and delete users within the app
- Posts: Fetched from JSONPlaceholder - a free fake REST API for testing and prototyping
When building your app:
- Replace the
BASE_URLinAppModule.ktwith your actual API endpoint- Update DTOs in
data/remote/model/to match your API response structure- Modify
ApiService.ktwith your actual API endpoints- Update Room entities and DAOs for your data models
The template follows Clean Architecture principles:
Presentation Layer (UI)
β
Domain Layer (Business Logic)
β
Data Layer (Repository Pattern)
β
Data Sources (Room, Ktor, DataStore)
- UI observes
StateFlowfrom ViewModel - ViewModel calls Repository/ApiService methods
- Repository coordinates between local (Room) and remote (Ktor) data sources
- Result type ensures type-safe error handling throughout
- Create route in
Routes.kt:
@Serializable
data class NewScreen(val param: String) : Route-
Create screen composable in
presentation/screens/ -
Add to
AppNavigation.kt:
is Route.NewScreen -> NavEntry(key) {
NewScreenContent(param = key.param)
}- Add DTO in
data/remote/model/:
@Serializable
data class ItemDto(val id: Int, val name: String)- Add method to
ApiService.kt:
suspend fun getItems(): Result<List<ItemDto>> =
networkClient.get("/items")- Create entity in
data/local/entity/:
@Entity(tableName = "items")
data class ItemEntity(
@PrimaryKey val id: Long,
val name: String
)- Create DAO in
data/local/dao/:
@Dao
interface ItemDao {
@Query("SELECT * FROM items")
fun getAll(): Flow<List<ItemEntity>>
}- Add to
AppDatabase.kt:
@Database(entities = [UserEntity::class, ItemEntity::class], ...)
abstract class AppDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
}- Register DAO in
AppModule.kt:
single { get<AppDatabase>().itemDao() }| Method | Description |
|---|---|
get<T>(endpoint) |
GET request returning Result<T> |
post<T, B>(endpoint, body) |
POST request with body |
put<T, B>(endpoint, body) |
PUT request with body |
patch<T, B>(endpoint, body) |
PATCH request with body |
delete<T>(endpoint) |
DELETE request |
getOrNull<T>(endpoint) |
GET returning T? (null on error) |
getOrThrow<T>(endpoint) |
GET returning T (throws on error) |
| Method | Description |
|---|---|
onSuccess { } |
Execute block on success |
onError { } |
Execute block on error |
map { } |
Transform success value |
flatMap { } |
Transform to another Result |
getOrNull() |
Get value or null |
getOrThrow() |
Get value or throw exception |
- Navigation 3 is in alpha - API may change in future releases
- Compose Multiplatform 1.10.0 is in beta - some features may be unstable
- Room's
@ConstructedByannotation shows beta warnings (can be suppressed) - kotlinx-datetime
Clock.Systemhas issues on iOS - using platform-specificcurrentTimeMillis()as workaround
This template is available under the MIT License. Feel free to use it for your projects.
Learn more about Kotlin Multiplatform