Clean Architecture
Semua project mobile di Bit Group Asia wajib mengikuti prinsip Clean Architecture untuk memastikan separation of concerns, testability, dan maintainability.
Prinsip Utama
- Independence of Frameworks — Arsitektur tidak bergantung pada framework tertentu.
- Testable — Business logic dapat ditest tanpa UI, database, atau API.
- Independence of UI — UI dapat berubah tanpa mengubah business logic.
- Independence of Database — Database dapat diganti tanpa mengubah business logic.
Layer Structure
Dependency Rule
Dependencies hanya boleh mengarah ke dalam (dari luar ke dalam).
- Presentation → boleh depend ke Domain
- Data → boleh depend ke Domain
- Domain → TIDAK BOLEH depend ke Presentation atau Data
Implementasi per Platform
- Android (Kotlin)
- iOS (Swift)
- Flutter (Dart)
- React Native (TS)
// Domain Layer - Use Case
class GetUserUseCase(
private val repository: UserRepository // Interface
) {
suspend operator fun invoke(id: String): Result<User> {
return repository.getUser(id)
}
}
// Domain Layer - Repository Interface
interface UserRepository {
suspend fun getUser(id: String): Result<User>
}
// Data Layer - Repository Implementation
class UserRepositoryImpl(
private val api: UserApiService,
private val dao: UserDao,
) : UserRepository {
override suspend fun getUser(id: String): Result<User> {
return try {
val dto = api.getUser(id)
dao.insertUser(dto.toEntity())
Result.success(dto.toDomain())
} catch (e: Exception) {
val cached = dao.getUser(id)
if (cached != null) Result.success(cached.toDomain())
else Result.failure(e)
}
}
}
// Domain Layer - Use Case
struct GetUserUseCase {
let repository: UserRepositoryProtocol
func execute(id: String) async throws -> User {
try await repository.getUser(id: id)
}
}
// Domain Layer - Repository Protocol
protocol UserRepositoryProtocol {
func getUser(id: String) async throws -> User
}
// Data Layer - Repository Implementation
final class UserRepository: UserRepositoryProtocol {
private let apiService: UserAPIService
private let localStore: UserLocalStore
func getUser(id: String) async throws -> User {
do {
let dto = try await apiService.getUser(id: id)
try localStore.save(dto.toEntity())
return dto.toDomain()
} catch {
if let cached = try? localStore.getUser(id: id) {
return cached.toDomain()
}
throw error
}
}
}
// Domain Layer - Use Case
class GetUserUseCase {
final UserRepository _repository;
GetUserUseCase(this._repository);
Future<User> call(String id) async {
return _repository.getUser(id);
}
}
// Domain Layer - Repository Interface
abstract class UserRepository {
Future<User> getUser(String id);
}
// Data Layer - Repository Implementation
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource _remote;
final UserLocalDataSource _local;
UserRepositoryImpl(this._remote, this._local);
Future<User> getUser(String id) async {
try {
final dto = await _remote.getUser(id);
await _local.cacheUser(dto);
return dto.toDomain();
} catch (e) {
final cached = await _local.getUser(id);
if (cached != null) return cached.toDomain();
rethrow;
}
}
}
// Domain Layer - Use Case
export class GetUserUseCase {
constructor(private repository: UserRepository) {}
async execute(id: string): Promise<User> {
return this.repository.getUser(id);
}
}
// Domain Layer - Repository Interface
export interface UserRepository {
getUser(id: string): Promise<User>;
}
// Data Layer - Repository Implementation
export class UserRepositoryImpl implements UserRepository {
constructor(
private api: UserApiService,
private storage: UserLocalStorage,
) {}
async getUser(id: string): Promise<User> {
try {
const dto = await this.api.getUser(id);
await this.storage.cacheUser(dto);
return mapToUser(dto);
} catch (error) {
const cached = await this.storage.getUser(id);
if (cached) return mapToUser(cached);
throw error;
}
}
}
Mapping
Setiap layer memiliki model sendiri:
- DTO (Data Transfer Object) — untuk komunikasi API
- Entity — untuk penyimpanan lokal
- Domain Model — untuk business logic dan presentation
Gunakan mapper/extension function untuk konversi antar model.