Skip to content

Repository files navigation

Compose Multiplatform Template

A production-ready Compose Multiplatform template with modern Android/iOS architecture, featuring Navigation 3, Room Database, DataStore, Koin DI, and Ktor networking.

Tech Stack

LibraryVersionPurpose
Compose Multiplatform1.10.0-beta02UI Framework
Kotlin2.2.21Language
Navigation 31.0.0-alpha05Type-safe Navigation
Room2.8.4Local Database
DataStore1.2.0Preferences Storage
Koin4.0.4Dependency Injection
Ktor3.1.3Networking
Kotlinx Serialization1.9.0JSON Parsing
Kotlinx Coroutines1.10.2Async Operations

Project Structure

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)

Features

Navigation 3

Type-safe navigation with serializable routes:

// Define routes
@Serializable
sealedinterfaceRoute {
@Serializable
data objectHome : Route
@Serializable
data classDetail(valid:String) : Route
}
// Navigate
backStack.add(Route.Detail(id ="123"))

Room Database (KMP)

Full Room support for both Android and iOS:

@Entity(tableName ="users")
data classUserEntity(
@PrimaryKey(autoGenerate =true)
valid:Long = 0,
valname:String,
valemail:String
)
@Dao
interfaceUserDao {
@Query("SELECT * FROM users")
fungetAllUsers(): Flow<List<UserEntity>>
@Insert
suspendfuninsertUser(user:UserEntity): Long
}

NetworkClient with Result Type

Type-safe networking with automatic error handling:

// Simple API callsclassApiService(privatevalnetworkClient:NetworkClient) {
suspendfungetPosts(): Result<List<PostDto>> =
networkClient.get("/posts")
suspendfuncreatePost(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()

Result Type

Comprehensive error handling:

sealedclassResult<outT> {
data classSuccess<T>(valdata:T) : Result<T>()
data classError(valexception:AppException) : Result<Nothing>()
data objectLoading : Result<Nothing>()
}
// Exception typessealedclassAppException {
data classNetworkException(...) // Connection issues
data classServerException(...) // 5xx errors
data classClientException(...) // 4xx errors
data classSerializationException(...) // JSON parsing
data classTimeoutException(...) // Request timeout
data classUnknownException(...) // Other errors
}

DataStore Preferences

Cross-platform preferences storage:

classPreferencesRepository(privatevaldataStore:DataStore<Preferences>) {
val darkModeFlow:Flow<Boolean> = dataStore.data.map {
it[PreferencesKeys.DARK_MODE] ?:false
}
suspendfunsetDarkMode(enabled:Boolean) {
dataStore.edit { it[PreferencesKeys.DARK_MODE] = enabled }
}
}

Koin Dependency Injection

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)
}

Getting Started

Prerequisites

  • Android Studio Ladybug or later
  • Xcode 15+ (for iOS)
  • JDK 17+

Build & Run

Android:

./gradlew :composeApp:assembleDebug

iOS:

./gradlew :composeApp:compileKotlinIosSimulatorArm64
# Then open iosApp/iosApp.xcodeproj in Xcode

Configuration

Update the base URL in AppModule.kt:

object ApiConfig {
constvalBASE_URL="https://your-api.com"
}

Sample Data Notice

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:

  1. Replace the BASE_URL in AppModule.kt with your actual API endpoint
  2. Update DTOs in data/remote/model/ to match your API response structure
  3. Modify ApiService.kt with your actual API endpoints
  4. Update Room entities and DAOs for your data models

Architecture

The template follows Clean Architecture principles:

Presentation Layer (UI)
↓
Domain Layer (Business Logic)
↓
Data Layer (Repository Pattern)
↓
Data Sources (Room, Ktor, DataStore)

Data Flow

  1. UI observes StateFlow from ViewModel
  2. ViewModel calls Repository/ApiService methods
  3. Repository coordinates between local (Room) and remote (Ktor) data sources
  4. Result type ensures type-safe error handling throughout

Customization Guide

Adding a New Screen

  1. Create route in Routes.kt:
@Serializable
data classNewScreen(valparam:String) : Route
  1. Create screen composable in presentation/screens/

  2. Add to AppNavigation.kt:

isRoute.NewScreen->NavEntry(key) {
NewScreenContent(param = key.param)
}

Adding a New API Endpoint

  1. Add DTO in data/remote/model/:
@Serializable
data classItemDto(valid:Int, valname:String)
  1. Add method to ApiService.kt:
suspendfungetItems(): Result<List<ItemDto>> =
networkClient.get("/items")

Adding a New Database Entity

  1. Create entity in data/local/entity/:
@Entity(tableName ="items")
data classItemEntity(
@PrimaryKey valid:Long,
valname:String
)
  1. Create DAO in data/local/dao/:
@Dao
interfaceItemDao {
@Query("SELECT * FROM items")
fungetAll(): Flow<List<ItemEntity>>
}
  1. Add to AppDatabase.kt:
@Database(entities = [UserEntity::class, ItemEntity::class], ...)
abstractclassAppDatabase : RoomDatabase() {
abstractfunitemDao(): ItemDao
}
  1. Register DAO in AppModule.kt:
single { get<AppDatabase>().itemDao() }

API Reference

NetworkClient Methods

MethodDescription
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)

Result Methods

MethodDescription
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

Known Limitations

  • 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 @ConstructedBy annotation shows beta warnings (can be suppressed)
  • kotlinx-datetime Clock.System has issues on iOS - using platform-specific currentTimeMillis() as workaround

License

This template is available under the MIT License. Feel free to use it for your projects.


Learn more about Kotlin Multiplatform

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages