Clean Architecture — vícevrstvá architektura navržená Robertem Martinem (Uncle Bob) v roce 2012, rozdělující aplikaci na nezávislé vrstvy: Domain (Entities, Use Cases), Data (Repositories, DataSources) a Presentation (ViewModels, Views). Hlavní princip — Dependency Rule: závislosti směřují dovnitř, vnější vrstvy závisí na vnitřních, ale ne naopak. Clean Architecture se používá v mobilním vývoji pro projekty s vysokou složitostí obchodní logiky. Více — v knize The Clean Architecture.
Hlavní body
Clean Architecture — architektonický vzor formulovaný Robertem Martinem (Uncle Bob) v roce 2012. Hlavní myšlenka — rozdělení aplikace na vrstvy s přísným pravidlem závislostí: kód uvnitř vrstvy neví o kódu vně. Vnější vrstvy (UI, frameworky, databáze) — detaily implementace. Vnitřní vrstvy (obchodní logika, podniková pravidla) — podstata aplikace.
Vrstvy Clean Architecture v mobilním vývoji: 1) Domain — Entities (obchodní objekty) a Use Cases (případy použití); 2) Data — RepositoryImpl (implementace repozitářů), DataSources (síť, databáze, cache); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain — nejvnitřnější vrstva, bez závislostí. Data závisí na Domain (implementuje rozhraní repozitářů). Presentation závisí na Domain (volá Use Cases, odebírá výsledek).
| Vrstva | Obsahuje | Závislosti |
|---|---|---|
| Domain | Entities, Use Cases, Repository Interfaces | Žádné (čistý Kotlin/Swift) |
| Data | RepositoryImpl, DataSources (API, DB, Cache) | Domain, Retrofit, Room, Ktor |
| Presentation | ViewModels, Views, Composables | Domain, Jetpack, SwiftUI |
Dependency Rule — jediné přísné pravidlo Clean Architecture. Zdrojový kód může odkazovat pouze na vrstvu uvnitř sebe nebo na vrstvu níže (blíže centru). Presentation importuje Domain. Domain NEimportuje Data ani Presentation. Toho se dosahuje pomocí invertování závislostí (Dependency Inversion Principle): Domain definuje rozhraní Repository, Data ho implementuje. Presentation závisí na abstrakci UseCase, nikoli na konkrétním repozitáři.
Domain — nejstabilnější vrstva aplikace. Entities — obchodní objekty nezávislé na frameworkách: User, Product, Order. Use Cases — třídy s jednou metodou invoke (nebo operator fun invoke v Kotlin), implementující jeden scénář: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces — abstrakce přístupu k datům, definované v Domain, implementované v Data. Domain neobsahuje Android SDK, iOS UIKit, Retrofit, Room — pouze čistý Kotlin nebo Swift.
// Entity — obchodní objekt (Domain)
struct User: Equatable {
let id: Int
let name: String
let email: String
}
// Repository Interface — abstrakce dat (Domain)
protocol UserRepository {
func getUser(id: Int) async throws -> User
func getUsers() async throws -> [User]
}
// Use Case — jeden scénář (Domain)
final class GetUserUseCase {
private let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
func execute(id: Int) async throws -> User {
return try await repository.getUser(id: id)
}
}
Use Case — «třída s jednou metodou» — není dogma, ale praktické doporučení. Když se Use Case stane složitějším (validace + logování + volání repozitáře), jeho metody se seskupují podle významu: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Důležité — Use Case by neměl vědět, odkud data pocházejí (síť, databáze, cache) a kdo je zobrazuje (Compose, SwiftUI). V IT Sectr přidělujeme Use Case pro každou operaci, která má obchodní pravidlo, validaci nebo kombinaci dat ze dvou zdrojů.
Čistota Domain se dosahuje pomocí DTO mapování na hranicích vrstev. Vrstva Data přijímá JSON modely (DTO), mapuje je na Domain Entity. Presentation přijímá Domain Entity, mapuje na ViewModel (DisplayItem). Domain Entity nikdy neobsahuje anotace Retrofit, Room, Codable — to zaručuje, že vrstvu nebude třeba měnit při přechodu z databáze Room na Realm nebo při nahrazení Retrofit za Ktor.
Data Layer — implementace rozhraní definovaných v Domain. Obsahuje RepositoryImpl (třídy implementující UserRepository) a DataSources (RemoteDataSource — API, LocalDataSource — databáze, CacheDataSource — SharedPreferences/NSUserDefaults). Vrstva Data závisí na Domain (importuje rozhraní repozitářů a Entities) a na frameworkách (Retrofit, Room, Ktor, CoreData). RepositoryImpl skrývá před Domain zdroj dat — Use Case neví, zda data pocházejí ze sítě nebo cache.
// DTO — model pro síť (Data)
data class UserDto(
@SerializedName("id") val id: Int,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("email") val email: String
)
// RepositoryImpl — implementace (Data)
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: Int): User {
// Pokoušíme se získat z cache
localDataSource.getUser(id)?.let { return it.toDomain() }
// Pokud není — načítáme ze sítě
val dto = remoteDataSource.fetchUser(id)
val user = dto.toDomain()
localDataSource.saveUser(user)
return user
}
override suspend fun getUsers(): List<User> {
return remoteDataSource.fetchAllUsers().map { it.toDomain() }
}
}
// Mapper — konverze DTO ↔ Domain
fun UserDto.toDomain() = User(
id = id,
name = "$firstName $lastName",
email = email
)
Strategie cache v Data Layer: RepositoryImpl nejprve zkontroluje místní úložiště, při neexistenci dat — načte ze sítě a uloží lokálně. Pokud síť není dostupná — vrátí zastaralá data s označením isStale. Use Case v Domain neví o strategii — přijímá User přes Repository.getUser(id). Změna strategie (např. invalidace cache každých 15 minut) neovlivní Domain a Presentation.
Modularita v Android — Kotlin Multiplatform umožňuje přesunout Domain do samostatného KMP modulu bez závislostí na Android SDK. Data — samostatný modul se závislostí na Domain. Presentation — Android modul se závislostí na Domain. Gradle závislosti: domain (pure Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Taková modularita je povinná pro velké projekty — CI staví Domain samostatně, unit testy Domain nevyžadují Android emulátor.
Presentation Layer — nejvnější vrstva Clean Architecture. Obsahuje ViewModels (Android) / ObservableObject (iOS) a Views (Compose/SwiftUI). ViewModel volá Use Case, přijímá výsledek a převádí ho na UI stav (State). View odebírá State a zobrazuje. Presentation závisí na Domain — importuje Use Cases a Entities. Presentation neimportuje Data Layer — data přicházejí přes Use Case, který interně používá Repository.
ViewModel v Clean Architecture neobsahuje obchodní logiku — volá Use Case. Pokud Use Case vrací User, ViewModel ho převede na UserDisplayItem (name, emailFormatted, avatarUrl) — čistě prezentační model. Use Case neví o DisplayItem — vrací Entity. Toto rozdělení umožňuje testovat Use Case bez UI a ViewModel bez UseCase (přes mock). V IT Sectr důsledně dodržujeme: Use Case — obchodní logika, ViewModel — pouze prezentace, View — pouze zobrazení.
// Use Case (Domain) — čistá obchodní logika
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: Int): User {
return repository.getUser(id)
}
}
// ViewModel (Presentation) — pouze prezentace
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _state = MutableStateFlow<UserScreenState>(UserScreenState.Loading)
val state: StateFlow<UserScreenState> = _state.asStateFlow()
fun loadUser(id: Int) {
viewModelScope.launch {
_state.value = UserScreenState.Loading
val user = getUserUseCase(id)
val displayItem = UserDisplayItem(
name = user.name,
email = user.email,
initials = user.name.split(" ").joinToString("") { it.first().toString() }
)
_state.value = UserScreenState.Success(displayItem)
}
}
}
data class UserDisplayItem(
val name: String,
val email: String,
val initials: String
)
sealed interface UserScreenState {
data object Loading : UserScreenState
data class Success(val displayItem: UserDisplayItem) : UserScreenState
data class Error(val message: String) : UserScreenState
}
Navigation ve vrstvě Presentation — součást vnějšího kruhu. Clean Architecture nepředepisuje mechanismus navigace — může to být NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) nebo Router (VIPER). Důležité: rozhodnutí o navigaci činí Presentation, ale navigace by neměla pronikat do Use Case. Use Case vrací výsledek, ViewModel rozhoduje, na kterou obrazovku přejít. V Clean Architecture je navigace detail, který lze vyměnit bez změny Domain.
Clean Architecture na Android se implementuje pomocí Gradle modulů: domain (pure Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Struktura složek: domain/user/User.kt, GetUserUseCase.kt, UserRepository.kt; data/remote/UserRemoteDataSource.kt, local/UserDao.kt, repository/UserRepositoryImpl.kt; presentation/ui/user/UserViewModel.kt, UserScreen.kt. DI (Hilt) propojuje vrstvy: UserRepositoryImpl je vázán na rozhraní UserRepository v domain modulu.
Clean Architecture na iOS používá SPM nebo Xcode skupiny bez samostatných modulů (kvůli omezením Xcode). Domain — složka se soubory, které neimportují UIKit nebo SwiftUI. Data — složka s APIClient, CoreDataStack, RepositoryImpl. Presentation — složka s ViewModels a SwiftUI Views. DI přes konstruktor nebo assembly v App. Hlavní volání — async/await přes UseCase.execute() s kontrolou MainActor pro UI aktualizace.
// Data Layer: Remote DataSource (iOS)
final class UserRemoteDataSource {
private let apiClient: APIClient
func fetchUser(id: Int) async throws -> UserDTO {
return try await apiClient.get("/users/\(id)")
}
}
// Repository Implementation (Data)
final class UserRepositoryImpl: UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
func getUser(id: Int) async throws -> User {
if let cached = try await local.getUser(id) {
return cached
}
let dto = try await remote.fetchUser(id)
let user = dto.toDomain()
try await local.saveUser(user)
return user
}
}
// Presentation: ViewModel + SwiftUI View
@MainActor
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserScreenState = .loading
private let getUserUseCase: GetUserUseCase
func loadUser(id: Int) {
Task {
state = .loading
if let user = try? await getUserUseCase.execute(id: id) {
state = .success(user)
} else {
state = .error("Failed to load")
}
}
}
}
Clean Architecture v projektech IT Sectr — náš standard pro projekty od 30 dnů. Používáme třívrstvou architekturu s Kotlin Multiplatform pro Android/iOS od roku 2022. Domain — společný KMP modul, Data — platformní moduly (Retrofit na Android, URLSession na iOS), Presentation — nativní UI. To poskytuje 60–80% společného kódu obchodní logiky mezi iOS a Android, zkracuje dobu vývoje o 30–40% ve srovnání se dvěma samostatnými implementacemi.
Často kladené otázky
Minimálně tři: Domain, Data, Presentation. Pro velké projekty se přidává Framework (závislosti Android SDK/iOS UIKit) a Device (GPS, kamera, senzory). Počet vrstev — není přísné pravidlo, ale otázka pohodlí. Důležité je dodržovat Dependency Rule: závislosti směřují dovnitř, k Domain. Lze začít se třemi a přidávat vrstvy s růstem projektu.
Ano — o 30–50% ve srovnání s MVVM kvůli oddělení rozhraní repozitářů, Use Cases a mapperů. Pro jednoduchou CRUD aplikaci je to nadbytečné. Clean Architecture je opodstatněná pro projekty se složitou obchodní logikou, kde je testovatelnost a izolace vrstev důležitější než rychlost vývoje. Pro MVP nebo prototyp používejte MVVM — Clean Architecture zpomalí spuštění.
Ano, je to běžná praxe. Use Cases zůstávají v Domain a Presentation používá MVI cyklus (Intent → Reducer → State). Data Layer — stejný, Domain — stejný. MVI v Presentation poskytuje předvídatelný stav obrazovky, Clean Architecture — izolaci obchodní logiky. Tato kombinace se používá ve velkých projektech s desítkami vývojářů.
Use Case je potřeba, když operace zahrnuje obchodní pravidlo: validaci, kombinaci dat ze dvou zdrojů, výpočet, logování, kontrolu přístupových práv. Jednoduchý požadavek getUser(id) bez dodatečné logiky může volat Repository přímo z ViewModel. Pro jednotnost architektury však mnoho týmů vytváří Use Case pro každou veřejnou metodu Repository — to přidá 5–10% kódu, ale zjednodušuje čtení.
Domain: unit testy Use Cases s mock Repository — čistý Kotlin/Swift bez Android SDK. Data: integrační testy RepositoryImpl s mock/fake DataSource. Presentation: testy ViewModel s mock UseCase. Díky Dependency Rule se každá vrstva testuje izolovaně. V IT Sectr pokrytí Domain dosahuje 95%, Data — 70–80%, Presentation — 60–70%.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také