Clean Architecture — arsitektur berlapis yang diusulkan oleh Robert Martin (Uncle Bob) pada tahun 2012, membagi aplikasi menjadi lapisan independen: Domain (Entities, Use Cases), Data (Repositories, DataSources) dan Presentation (ViewModels, Views). Prinsip utama — Dependency Rule: dependensi diarahkan ke dalam, lapisan eksternal bergantung pada lapisan internal, bukan sebaliknya. Clean Architecture diterapkan dalam pengembangan mobile untuk proyek dengan kompleksitas logika bisnis yang tinggi. Selengkapnya — di buku The Clean Architecture.
Poin Utama
Clean Architecture — pola arsitektur yang dirumuskan oleh Robert Martin (Uncle Bob) pada tahun 2012. Ide utama — pembagian aplikasi menjadi lapisan dengan aturan dependensi yang ketat: kode di dalam lapisan tidak tahu tentang kode di luar. Lapisan eksternal (UI, framework, database) — detail implementasi. Lapisan internal (logika bisnis, aturan perusahaan) — esensi aplikasi.
Lapisan Clean Architecture dalam pengembangan mobile: 1) Domain — Entities (objek bisnis) dan Use Cases (kasus penggunaan); 2) Data — RepositoryImpl (implementasi repositori), DataSources (jaringan, database, cache); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain — lapisan paling dalam, tanpa dependensi. Data bergantung pada Domain (mengimplementasikan antarmuka repositori). Presentation bergantung pada Domain (memanggil Use Cases, berlangganan hasil).
| Lapisan | Berisi | Dependensi |
|---|---|---|
| Domain | Entities, Use Cases, Repository Interfaces | Tidak ada (Kotlin/Swift murni) |
| Data | RepositoryImpl, DataSources (API, DB, Cache) | Domain, Retrofit, Room, Ktor |
| Presentation | ViewModels, Views, Composables | Domain, Jetpack, SwiftUI |
Dependency Rule — satu-satunya aturan ketat Clean Architecture. Kode sumber hanya dapat merujuk ke lapisan di dalamnya atau ke lapisan di bawahnya (lebih dekat ke pusat). Presentation mengimpor Domain. Domain TIDAK mengimpor Data atau Presentation. Ini dicapai melalui pembalikan dependensi (Dependency Inversion Principle): Domain mendefinisikan antarmuka Repository, Data mengimplementasikannya. Presentation bergantung pada abstraksi UseCase, bukan pada repositori konkret.
Domain — lapisan paling stabil dari aplikasi. Entities — objek bisnis tidak tergantung pada framework: User, Product, Order. Use Cases — kelas dengan satu metode invoke (atau operator fun invoke di Kotlin), mengimplementasikan satu skenario: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces — abstraksi akses data, didefinisikan di Domain, diimplementasikan di Data. Domain tidak mengandung Android SDK, iOS UIKit, Retrofit, Room — hanya Kotlin atau Swift murni.
// Entity — objek bisnis (Domain)
struct User: Equatable {
let id: Int
let name: String
let email: String
}
// Repository Interface — abstraksi data (Domain)
protocol UserRepository {
func getUser(id: Int) async throws -> User
func getUsers() async throws -> [User]
}
// Use Case — satu skenario (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 — «kelas dengan satu metode» — bukan dogma, melainkan rekomendasi praktis. Ketika Use Case menjadi lebih kompleks (validasi + logging + panggilan repositori), metode-metodenya dikelompokkan berdasarkan arti: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Penting — Use Case tidak boleh tahu dari mana data berasal (jaringan, database, cache) dan siapa yang menampilkannya (Compose, SwiftUI). Di IT Sectr kami mengalokasikan Use Case untuk setiap operasi yang memiliki aturan bisnis, validasi, atau penggabungan data dari dua sumber.
Kemurnian Domain dicapai melalui pemetaan DTO di batas lapisan. Lapisan Data menerima model JSON (DTO), memetakannya ke Entity Domain. Presentation menerima Entity Domain, memetakannya ke ViewModel (DisplayItem). Entity Domain tidak pernah mengandung anotasi Retrofit, Room, Codable — ini menjamin bahwa lapisan tidak perlu diubah saat mengganti database dari Room ke Realm atau saat mengganti Retrofit dengan Ktor.
Data Layer — implementasi antarmuka yang didefinisikan di Domain. Berisi RepositoryImpl (kelas yang mengimplementasikan UserRepository) dan DataSources (RemoteDataSource — API, LocalDataSource — database, CacheDataSource — SharedPreferences/NSUserDefaults). Lapisan Data bergantung pada Domain (mengimpor antarmuka repositori dan Entities) dan pada framework (Retrofit, Room, Ktor, CoreData). RepositoryImpl menyembunyikan sumber data dari Domain — Use Case tidak tahu apakah data berasal dari jaringan atau cache.
// DTO — model untuk jaringan (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 — implementasi (Data)
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: Int): User {
// Mencoba mengambil dari cache
localDataSource.getUser(id)?.let { return it.toDomain() }
// Jika tidak ada — memuat dari jaringan
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 — konversi DTO ↔ Domain
fun UserDto.toDomain() = User(
id = id,
name = "$firstName $lastName",
email = email
)
Strategi caching di Data Layer: RepositoryImpl pertama memeriksa penyimpanan lokal, jika tidak ada data — memuat dari jaringan dan menyimpan secara lokal. Jika jaringan tidak tersedia — mengembalikan data usang dengan tanda isStale. Use Case di Domain tidak tahu tentang strategi — menerima User melalui Repository.getUser(id). Perubahan strategi (misalnya, invalidasi cache setiap 15 menit) tidak mempengaruhi Domain dan Presentation.
Modularitas di Android — Kotlin Multiplatform memungkinkan Domain dipindahkan ke modul KMP terpisah tanpa dependensi Android SDK. Data — modul terpisah dengan dependensi pada Domain. Presentation — modul Android dengan dependensi pada Domain. Dependensi Gradle: domain (pure Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Modularitas seperti ini wajib untuk proyek besar — CI membangun Domain secara terpisah, unit test Domain tidak memerlukan emulator Android.
Presentation Layer — lapisan terluar dari Clean Architecture. Berisi ViewModels (Android) / ObservableObject (iOS) dan Views (Compose/SwiftUI). ViewModel memanggil Use Case, menerima hasil dan mengubahnya menjadi status UI (State). View berlangganan State dan menampilkan. Presentation bergantung pada Domain — mengimpor Use Cases dan Entities. Presentation tidak mengimpor Data Layer — data datang melalui Use Case, yang secara internal menggunakan Repository.
ViewModel di Clean Architecture tidak mengandung logika bisnis — memanggil Use Case. Jika Use Case mengembalikan User, ViewModel mengubahnya menjadi UserDisplayItem (name, emailFormatted, avatarUrl) — model presentasi murni. Use Case tidak tahu tentang DisplayItem — mengembalikan Entity. Pemisahan ini memungkinkan pengujian Use Case tanpa UI dan ViewModel tanpa UseCase (melalui mock). Di IT Sectr kami secara ketat mematuhi: Use Case — logika bisnis, ViewModel — hanya presentasi, View — hanya tampilan.
// Use Case (Domain) — logika bisnis murni
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: Int): User {
return repository.getUser(id)
}
}
// ViewModel (Presentation) — hanya presentasi
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 di lapisan Presentation — bagian dari cincin luar. Clean Architecture tidak menentukan mekanisme navigasi — bisa NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) atau Router (VIPER). Penting: keputusan navigasi dibuat oleh Presentation, tetapi navigasi tidak boleh menembus ke Use Case. Use Case mengembalikan hasil, ViewModel memutuskan ke layar mana akan pergi. Di Clean Architecture navigasi adalah detail yang dapat diganti tanpa mengubah Domain.
Clean Architecture di Android diimplementasikan melalui modul Gradle: domain (pure Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Struktur folder: 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) menghubungkan lapisan: UserRepositoryImpl diikat ke antarmuka UserRepository di modul domain.
Clean Architecture di iOS menggunakan SPM atau grup Xcode tanpa modul terpisah (karena keterbatasan Xcode). Domain — folder dengan file yang tidak mengimpor UIKit atau SwiftUI. Data — folder dengan APIClient, CoreDataStack, RepositoryImpl. Presentation — folder dengan ViewModels dan SwiftUI Views. DI melalui konstruktor atau assembly di App. Panggilan utama — async/await melalui UseCase.execute() dengan pemeriksaan MainActor untuk pembaruan UI.
// 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 di proyek IT Sectr — standar kami untuk proyek mulai 30 hari. Kami menggunakan arsitektur tiga lapis dengan Kotlin Multiplatform untuk Android/iOS sejak 2022. Domain — modul KMP bersama, Data — modul platform (Retrofit di Android, URLSession di iOS), Presentation — UI native. Ini memberikan 60–80% kode bersama logika bisnis antara iOS dan Android, mengurangi waktu pengembangan 30–40% dibandingkan dengan dua implementasi terpisah.
Pertanyaan Umum
Minimal tiga: Domain, Data, Presentation. Untuk proyek besar ditambahkan Framework (dependensi Android SDK/iOS UIKit) dan Device (GPS, kamera, sensor). Jumlah lapisan — bukan aturan ketat, masalah kenyamanan. Yang penting adalah mematuhi Dependency Rule: dependensi diarahkan ke dalam, ke Domain. Bisa mulai dengan tiga dan menambah lapisan seiring pertumbuhan proyek.
Ya — 30–50% dibandingkan MVVM karena pemisahan antarmuka repositori, Use Cases dan mapper. Untuk aplikasi CRUD sederhana ini berlebihan. Clean Architecture dibenarkan untuk proyek dengan logika bisnis kompleks, di mana testabilitas dan isolasi lapisan lebih penting daripada kecepatan pengembangan. Untuk MVP atau prototipe gunakan MVVM — Clean Architecture akan memperlambat peluncuran.
Ya, ini adalah praktik umum. Use Cases tetap di Domain dan Presentation menggunakan siklus MVI (Intent → Reducer → State). Data Layer — sama, Domain — sama. MVI di Presentation memberikan status layar yang dapat diprediksi, Clean Architecture — isolasi logika bisnis. Kombinasi ini digunakan dalam proyek besar dengan puluhan pengembang.
Use Case diperlukan ketika operasi mencakup aturan bisnis: validasi, penggabungan data dari dua sumber, perhitungan, logging, pemeriksaan hak akses. Permintaan getUser(id) sederhana tanpa logika tambahan dapat memanggil Repository langsung dari ViewModel. Namun untuk keseragaman arsitektur, banyak tim membuat Use Case untuk setiap metode publik Repository — ini menambah 5–10% kode tetapi menyederhanakan pembacaan.
Domain: unit test Use Cases dengan mock Repository — Kotlin/Swift murni tanpa Android SDK. Data: tes integrasi RepositoryImpl dengan mock/fake DataSource. Presentation: tes ViewModel dengan mock UseCase. Berkat Dependency Rule setiap lapisan diuji secara terisolasi. Di IT Sectr cakupan Domain mencapai 95%, Data — 70–80%, Presentation — 60–70%.
Ringkasan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga