Реальный пример: общий слой данных
Соберём изученное в один связный слой данных, который одинаково обслуживает Android и iOS.
Общий слой данных — связка из моделей (DTO/domain), API-клиента, базы и репозитория в общем коде, предоставляющая UI единый, кэшируемый источник данных.
Что строим
Соберём end-to-end слой для списка заказов: модель, сетевой запрос, локальный кэш в БД, репозиторий с политикой «сначала кэш, потом сеть». Весь этот код — в commonMain, оба нативных UI используют его как есть.
Модели
// commonMain — DTO от API и доменная модель
@Serializable
data class OrderDto(val id: Long, @SerialName("total_amount") val total: Double)
data class Order(val id: Long, val total: Double)
fun OrderDto.toDomain() = Order(id = id, total = total)API-клиент
// commonMain
class OrderApi(private val client: HttpClient) {
suspend fun fetchOrders(): List<OrderDto> =
client.get("https://api.example.com/orders").body()
}Репозиторий с кэшем
// commonMain
class OrderRepository(
private val api: OrderApi,
private val cache: OrderCache // поверх SQLDelight
) {
fun orders(): Flow<List<Order>> = cache.observe()
suspend fun refresh() {
val fresh = api.fetchOrders().map { it.toDomain() }
cache.replaceAll(fresh)
}
}UI подписывается на orders() и видит кэш мгновенно, а refresh() подтягивает свежие данные в фоне — обе платформы получают эту логику бесплатно.
Кэш на SQLDelight
// commonMain — обёртка над сгенерированными запросами
class OrderCache(private val db: AppDatabase) {
fun observe(): Flow<List<Order>> =
db.orderQueries.selectAll().asFlow().mapToList()
.map { rows -> rows.map { Order(it.id, it.total) } }
fun replaceAll(orders: List<Order>) {
db.transaction {
db.orderQueries.deleteAll()
orders.forEach { db.orderQueries.insertOrder(it.id, it.total) }
}
}
}Как работает под капотом
Связка реализует паттерн single source of truth: источник истины для UI — база (Flow из SQLDelight), а сеть лишь обновляет базу. UI никогда не ходит в сеть напрямую — он наблюдает кэш. Когда refresh() заменяет данные в транзакции, SQLDelight уведомляет подписчиков Flow, и оба UI перерисовываются. Вся эта механика — общая; платформам остаётся только отрисовка. Это наглядно показывает, какую долю приложения покрывает общий код.
Частые ошибки
Дать UI ходить и в сеть, и в кэш — тогда теряется единый источник истины и появляются гонки. UI должен наблюдать только кэш. Вторая ошибка — смешивать DTO и доменные модели; маппинг toDomain() изолирует формат API. Третья — не оборачивать replaceAll в транзакцию, рискуя показать UI промежуточное полупустое состояние.
Итоги
- Общий слой данных: модели, API, кэш и репозиторий — всё в
commonMain. - Паттерн single source of truth: UI наблюдает кэш, сеть обновляет кэш.
- Оба нативных UI получают одинаковую логику загрузки и кэширования бесплатно.
- Транзакции и маппинг DTO→domain держат данные консистентными.