Halo Developer! ๐
Di artikel sebelumnya kita sudah belajar GetX untuk state management, routing, dan dependency injection. Sekarang waktunya naik level ke arsitektur yang lebih rapi: Repository Pattern dalam Clean Architecture.
Bayangkan้กน็ฎ๏ผproject๏ผkamu tumbuh dari 5 halaman jadi 50 halaman. Tanpa arsitektur yang jelas, semua logic โ API call, parsing, UI โ menumpuk di satu file. Rรฉpot kan? Repository Pattern adalah solusinya.
Kenapa Clean Architecture?
Clean Architecture membagi aplikasi ke dalam layer terpisah:
- Presentation Layer โ Widget, UI, state management (GetX, Provider, dll)
- Domain Layer โ Business logic, entities, repository interface
- Data Layer โ Implementasi repository (API, database, cache)
Keuntungannya:
- Testable โ Kamu bisa unit test logic tanpa jalankan UI
- Swapable โ Ganti API ke database? Tinggal ganti implementasi, UI tetap sama
- Readable โ Setiap file punya satu tanggung jawab
Struktur Folder Clean Architecture
lib/
โโโ main.dart
โโโ core/
โ โโโ errors/
โ โ โโโ exceptions.dart
โ โโโ network/
โ โโโ api_service.dart
โโโ features/
โ โโโ users/
โ โโโ data/
โ โ โโโ models/
โ โ โ โโโ user_model.dart
โ โ โโโ repositories/
โ โ โ โโโ user_repository_impl.dart
โ โ โโโ datasources/
โ โ โโโ user_remote_datasource.dart
โ โ โโโ user_local_datasource.dart
โ โโโ domain/
โ โ โโโ entities/
โ โ โ โโโ user_entity.dart
โ โ โโโ repositories/
โ โ โ โโโ user_repository.dart
โ โ โโโ usecases/
โ โ โโโ get_users.dart
โ โโโ presentation/
โ โโโ controllers/
โ โ โโโ user_controller.dart
โ โโโ pages/
โ โโโ user_list_page.dart
Terdengar kompleks? Tenang, kita bangun dari yang sederhana dulu.
Contoh 1: Entity & Model
Entity adalah definisi data mentah tanpa dependensi ke framework:
// lib/features/users/domain/entities/user_entity.dart
class UserEntity {
final int id;
final String name;
final String email;
final String avatarUrl;
const UserEntity({
required this.id,
required this.name,
required this.email,
this.avatarUrl = '',
});
}
Model adalah representasi data dari API/JSON yang mengonversi ke Entity:
// lib/features/users/data/models/user_model.dart
import '../domain/entities/user_entity.dart';
class UserModel extends UserEntity {
const UserModel({
required super.id,
required super.name,
required super.email,
super.avatarUrl,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
avatarUrl: json['avatar_url'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'avatar_url': avatarUrl,
};
}
}
Perbedaan Entity vs Model? Entity cuma punya field data. Model punya
fromJson()/toJson()dan aware dengan format data spesifik (API response).
Contoh 2: Repository Interface & Implementasi
Repository Interface ada di domain layer โ ini kontrak yang harus dipenuhi:
// lib/features/users/domain/repositories/user_repository.dart
import '../entities/user_entity.dart';
abstract class UserRepository {
Future<List<UserEntity>> getUsers();
Future<UserEntity> getUserById(int id);
}
Repository Implementation ada di data layer โ yang eksekusi beneran:
// lib/features/users/data/repositories/user_repository_impl.dart
import '../datasources/user_remote_datasource.dart';
import '../models/user_model.dart';
import '../../domain/entities/user_entity.dart';
import '../../domain/repositories/user_repository.dart';
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
UserRepositoryImpl({required this.remoteDataSource});
@override
Future<List<UserEntity>> getUsers() async {
try {
final List<UserModel> users = await remoteDataSource.fetchUsers();
return users; // UserModel IS-A UserEntity
} catch (e) {
throw ServerException(message: 'Gagal mengambil data user');
}
}
@override
Future<UserEntity> getUserById(int id) async {
try {
final UserModel user = await remoteDataSource.fetchUserById(id);
return user;
} catch (e) {
throw ServerException(message: 'User tidak ditemukan');
}
}
}
Remote DataSource bertugas komunikasi langsung dengan API:
// lib/features/users/data/datasources/user_remote_datasource.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/user_model.dart';
class UserRemoteDataSource {
final String baseUrl;
UserRemoteDataSource({this.baseUrl = 'https://jsonplaceholder.typicode.com'});
Future<List<UserModel>> fetchUsers() async {
final response = await http.get(Uri.parse('$baseUrl/users'));
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => UserModel.fromJson(json)).toList();
}
throw Exception('Gagal fetch users');
}
Future<UserModel> fetchUserById(int id) async {
final response = await http.get(Uri.parse('$baseUrl/users/$id'));
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
}
throw Exception('User tidak ditemukan');
}
}
Menggunakan Repository di Controller (GetX)
Sekarang kita hubungkan ke presentation layer pakai GetX:
// lib/features/users/presentation/controllers/user_controller.dart
import 'package:get/get.dart';
import '../../domain/entities/user_entity.dart';
import '../../domain/repositories/user_repository.dart';
class UserController extends GetxController {
final UserRepository userRepository;
UserController({required this.userRepository});
final users = <UserEntity>[].obs;
final isLoading = false.obs;
final errorMessage = ''.obs;
@override
void onInit() {
super.onInit();
fetchUsers();
}
Future<void> fetchUsers() async {
isLoading.value = true;
errorMessage.value = '';
try {
final result = await userRepository.getUsers();
users.assignAll(result);
} catch (e) {
errorMessage.value = 'Terjadi kesalahan: $e';
} finally {
isLoading.value = false;
}
}
}
Wiring: Connect Everything
Di main.dart atau file setup:
import 'package:get/get.dart';
import 'features/users/data/datasources/user_remote_datasource.dart';
import 'features/users/data/repositories/user_repository_impl.dart';
import 'features/users/presentation/controllers/user_controller.dart';
import 'features/users/presentation/pages/user_list_page.dart';
void main() {
// DataSource
final remoteDataSource = UserRemoteDataSource();
// Repository (interface โ impl)
final userRepository = UserRepositoryImpl(
remoteDataSource: remoteDataSource,
);
// Controller
Get.put<UserController>(
UserController(userRepository: userRepository),
);
runApp(const MyApp());
}
Karena
UserRepositoryImplimplementsUserRepository, kita bisa pakai interface di controller. Kalau nanti mau pakai local database, tinggal buatUserRepositoryLocalImpldan ganti โ controller tidak perlu diubah.
Alur Visual Clean Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PRESENTATION โ
โ UserListPage โ UserController (GetX) โ
โ (Widget) (state + logic) โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ
โ calls
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DOMAIN โ
โ GetUserUseCase โ UserRepository (abstract) โ
โ UserEntity โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ
โ implements
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DATA โ
โ UserRepositoryImpl โ RemoteDataSource โ
โ UserModel (fromJson/toJson) โ
โ API / Database / Cache โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Arah dependency: Presentation โ Domain โ Data. Domain tidak tahu dari mana data datang.
Best Practice
- Interface di Domain, Implementasi di Data โ agar domain layer bersih dari dependensi teknis
- Satu Repository per Feature โ
UserRepository,ProductRepository,OrderRepository - Jangan skip Entity โ meski mirip Model, Entity menjaga domain tetap independen
- Exception handling di layer Data โ jangan biarkan raw exception bocor ke UI
- ** pakai
Eitheratau Result type** untuk error handling yang lebih rapi (bisa pakaifpdartpackage)
Kesimpulan
| Konsep | Layer | Tugas |
|---|---|---|
| Entity | Domain | Definisi data murni |
| Repository Interface | Domain | Kontrak akses data |
| Repository Impl | Data | Eksekusi akses data |
| DataSource | Data | Komunikasi langsung (API/DB) |
| Controller | Presentation | State + logic |
| Widget | Presentation | UI |
Repository Pattern memang butuh setup lebih banyak di awal, tapi investasi ini terbayar saat project tumbuh. Kode jadi terstruktur, mudah di-test, dan siap di-scale.
Coba sendiri! Buat mini project dengan Clean Architecture dari awal. Mulai dari feature sederhana seperti User List pakai API public seperti JSONPlaceholder. Share ke sosial media dan tag @ahsai001 ๐