Skip to content

Latest commit

 

History

History
360 lines (294 loc) · 9.42 KB

File metadata and controls

360 lines (294 loc) · 9.42 KB

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

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

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
sealed interface Route {
    @Serializable
    data object Home : Route

    @Serializable
    data class Detail(val id: String) : Route
}

// Navigate
backStack.add(Route.Detail(id = "123"))

Room Database (KMP)

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
}

NetworkClient with Result Type

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

Result Type

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
}

DataStore Preferences

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

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 {
    const val BASE_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 class NewScreen(val param: String) : Route
  1. Create screen composable in presentation/screens/

  2. Add to AppNavigation.kt:

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

Adding a New API Endpoint

  1. Add DTO in data/remote/model/:
@Serializable
data class ItemDto(val id: Int, val name: String)
  1. Add method to ApiService.kt:
suspend fun getItems(): Result<List<ItemDto>> =
    networkClient.get("/items")

Adding a New Database Entity

  1. Create entity in data/local/entity/:
@Entity(tableName = "items")
data class ItemEntity(
    @PrimaryKey val id: Long,
    val name: String
)
  1. Create DAO in data/local/dao/:
@Dao
interface ItemDao {
    @Query("SELECT * FROM items")
    fun getAll(): Flow<List<ItemEntity>>
}
  1. Add to AppDatabase.kt:
@Database(entities = [UserEntity::class, ItemEntity::class], ...)
abstract class AppDatabase : RoomDatabase() {
    abstract fun itemDao(): ItemDao
}
  1. Register DAO in AppModule.kt:
single { get<AppDatabase>().itemDao() }

API Reference

NetworkClient Methods

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)

Result Methods

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

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