Kamu telah menempuh lima bab. Sekarang semua keping pengetahuan itu kita rangkai menjadi satu arsitektur yang rapi dan profesional.
Di BAB 1–5 kamu belajar potongan-potongan keahlian. Clean Architecture adalah cara menyusun potongan itu agar aplikasi mudah dirawat, diuji, dan dikembangkan oleh tim besar.
Bayangkan sebuah rumah. Rumah yang baik dibangun berlapis: dari pondasi, lalu dinding, baru interior.
🏠 Rumah dibangun lapis demi lapis
Kalau kamu mau ganti cat dinding (UI), kamu tidak perlu membongkar pondasi. Itulah kekuatan pemisahan layer.
Data mengalir dari atas ke bawah saat meminta, lalu kembali ke atas saat membawa hasil. Setiap lapisan hanya bicara dengan tetangganya.
Widget, Page, State (BLoC/Provider) — apa yang dilihat user
UseCase, Entity, Repository Interface — aturan bisnis murni
Repository Impl, DataSource, Model — sumber & penyimpanan data
Aturan emas: Presentation → Domain → Data. Lapisan dalam (Domain) TIDAK BOLEH tahu soal lapisan luar (UI/Data spesifik).
Lapisan yang berurusan dengan tampilan dan interaksi user.
Pramusaji menyajikan makanan ke pelanggan dan mencatat pesanan. Ia tidak memasak. Tugasnya hanya komunikasi: ambil pesanan → teruskan ke dapur → antar hasil.
Begitu pula Widget: ia menampilkan data & menangkap tap user, lalu meneruskan ke Domain. Ia tidak menghitung logika bisnis.
// Presentation: hanya memanggil, tidak mengolah
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
context.read<AuthController>()
.login(email, password);
},
child: const Text('Masuk'),
);
}
}
Widget memanggil AuthController — tidak tahu cara kerja login di baliknya.
Jantung aplikasi: berisi aturan bisnis murni, bebas dari Flutter maupun sumber data.
Kepala dapur tahu resep dan aturan: berapa takaran, urutan memasak, standar rasa. Ia tidak peduli bahan datang dari pasar A atau supplier B — yang penting bahannya sesuai spesifikasi.
Domain berisi UseCase (aksi bisnis), Entity (objek inti), dan Repository Interface (kontrak, bukan implementasi).
// Entity — objek bisnis murni
class User {
final String id, name, email;
User(this.id, this.name, this.email);
}
// Kontrak — implementasi ada di Data Layer
abstract class AuthRepository {
Future<User> login(String email, String pass);
}
// UseCase — satu aksi bisnis
class LoginUseCase {
final AuthRepository repo;
LoginUseCase(this.repo);
Future<User> call(String e, String p)
=> repo.login(e, p);
}
Tidak ada import 'package:flutter' di sini. Murni Dart.
Lapisan yang benar-benar mengambil & menyimpan data dari API atau database lokal.
Gudang menyimpan stok dan tahu di rak mana barang berada. Saat dapur minta bahan, forklift mengambilnya dari rak (API) atau lemari pendingin (cache lokal).
Data Layer berisi Repository Implementation, RemoteDataSource (API), dan LocalDataSource (database).
// Implementasi nyata dari kontrak Domain
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remote;
AuthRepositoryImpl(this.remote);
@override
Future<User> login(String e, String p) async {
final model = await remote.login(e, p);
return User(model.id, model.name, model.email);
}
}
class AuthRemoteDataSource {
Future<UserModel> login(String e, String p) async {
final res = await dio.post('/login', data: {...});
return UserModel.fromJson(res.data);
}
}
Di sinilah JSON & REST API dari BAB 1–2 kembali dipakai.
Ikuti perjalanan satu tap tombol "Masuk" menembus seluruh lapisan, lalu kembali membawa hasil.
context.read<AuthController>().login() — status: loading
LoginUseCase memanggil AuthRepository.login()
POST /login → server membalas JSON
User object diterima — status: success → tampilkan Home
Pola yang menyembunyikan sumber data dari pengguna data.
Saat mencari rumah, kamu bicara ke satu agen. Kamu tidak perlu tahu apakah rumah itu dari developer A, lelang bank, atau penjual perorangan. Agen yang mengurus sumbernya.
Repository = agen properti. Domain cukup berkata "beri aku User", tanpa peduli datang dari API, cache, atau data dummy.
Widget langsung panggil Dio/http → kacau, sulit dites.
DENGAN REPOSITORYWidget → UseCase → Repository → sumber. Rapi & fleksibel.
Satu interface, dua implementasi yang bisa ditukar: nyata vs mock.
// === DOMAIN: kontrak ===
abstract class UserRepository {
Future<User> getUser(String id);
}
// === DATA: implementasi nyata ===
class UserRepositoryImpl implements UserRepository {
final ApiClient api;
UserRepositoryImpl(this.api);
@override
Future<User> getUser(String id) async {
final json = await api.get('/users/$id');
return UserModel.fromJson(json).toEntity();
}
}
// === Mock untuk testing / offline ===
class UserRepositoryMock implements UserRepository {
@override
Future<User> getUser(String id) async {
await Future.delayed(
Duration(milliseconds: 300));
return User(id, 'Budi Dummy',
'budi@test.com');
}
}
// Saat dipakai: tinggal swap satu baris!
UserRepository repo = UserRepositoryMock();
// atau ⇩ tanpa ubah kode lain
UserRepository repo = UserRepositoryImpl(api);
UI & UseCase tak berubah sedikit pun — itulah kekuatan abstraksi.
Membungkus sebuah kemampuan teknis (auth, notifikasi, lokasi) menjadi layanan yang rapi.
Kamu pesan lewat aplikasi: "antar ke alamat X". Driver menerima order. Driver tidak peduli kamu siapa atau dari mana ordernya — ia hanya menjalankan layanan antar.
Service membungkus aksi teknis agar bisa dipakai berkali-kali oleh bagian mana pun aplikasi.
class AuthService {
final FlutterSecureStorage storage;
AuthService(this.storage);
Future<void> saveToken(String t) =>
storage.write(key: 'token', value: t);
Future<String?> getToken() =>
storage.read(key: 'token');
}
class NotificationService {
Future<void> show(String msg) async {
// tampilkan notifikasi lokal
}
}
Repository fokus ke data. Service fokus ke kemampuan teknis.
Daripada setiap kelas membuat sendiri kebutuhannya, kita "suntikkan" dari luar.
Kamu butuh palu? Tidak perlu menempa besi sendiri. Cukup ambil dari rak toko. Toko sudah menyediakannya, siap pakai.
DI adalah "toko" berisi objek siap pakai (repository, service). Kelas tinggal minta, tidak membuat sendiri.
Di Flutter, tools populer untuk DI:
DI membuat dependency menjadi terpusat, sekali daftar, pakai di mana saja.
Lihat bedanya: membuat manual (kacau) vs mengambil dari locator (rapi).
import 'package:get_it/get_it.dart';
final sl = GetIt.instance;
void setupLocator() {
// 1. Service & client dasar
sl.registerLazySingleton(() => ApiClient());
// 2. Repository (butuh ApiClient)
sl.registerLazySingleton<UserRepository>(
() => UserRepositoryImpl(sl()),
);
// 3. UseCase (butuh Repository)
sl.registerFactory(
() => GetUserUseCase(sl()),
);
}
Panggil setupLocator() sekali di main().
// ❌ TANPA DI — buat manual, berantai
final api = ApiClient();
final repo = UserRepositoryImpl(api);
final uc = GetUserUseCase(repo);
// diulang di setiap file... 😫
// ✅ DENGAN DI — tinggal ambil
final uc = sl<GetUserUseCase>();
final user = await uc('42');
// Riverpod versi DI:
final userRepoProvider = Provider(
(ref) => UserRepositoryImpl(ref.read(apiProvider)),
);
Pohon folder yang konsisten membuat proyek besar tetap mudah dinavigasi seluruh tim.
Pendekatan ini disebut feature-first: tiap fitur adalah modul mandiri. Cocok untuk tim besar dan aplikasi yang terus tumbuh.
Setiap fitur memuat tiga layer Clean Architecture secara lengkap dan terpisah.
Perhatikan ada folder repositories di DUA tempat:
Inilah penerapan fisik dari prinsip: Domain mendefinisikan kontrak, Data memenuhinya.
Pola folder ini diulang sama persis untuk setiap fitur → konsisten & mudah ditebak.
Kita bangun fitur "Daftar Tugas" dengan Clean Architecture, dari layer terdalam ke terluar.
TodoModel, RemoteDataSource (ambil dari API), TodoRepositoryImpl
Entity Todo, TodoRepository (kontrak), GetTodos & AddTodo UseCase
TodoBloc/Provider, TodoPage, TodoTile widget
Urutan membangun: Domain (definisi) → Data (implementasi) → Presentation (tampilan). Tiga slide berikut adalah kode lengkapnya.
Setelah mengetik ulang kode 3 slide berikut, kamu akan punya satu fitur utuh yang menerapkan semua konsep BAB ini sekaligus.
Mulai dari inti: Entity, kontrak Repository, dan UseCase. Murni Dart.
Kita mulai dari Domain karena ia adalah inti yang tidak bergantung pada apa pun.
Todo — objek bisnis murni. Hanya berisi data penting (id, title, done), tanpa fromJson atau Flutter.TodoRepository — abstract class = janji/kontrak. Mengatakan "akan ada getTodos & addTodo", tapi belum bilang caranya.GetTodos & AddTodo — UseCase, masing-masing satu aksi bisnis. Method call() membuatnya bisa dipanggil seperti fungsi: getTodos().Perhatikan: UseCase hanya tahu kontrak (TodoRepository), bukan implementasinya. Itulah kunci agar Domain tetap bersih.
⏭️ Slide berikutnya: kita penuhi kontrak ini di Data Layer.
// === entities/todo.dart ===
class Todo {
final String id;
final String title;
final bool done;
const Todo({required this.id, required this.title, this.done = false});
}
// === repositories/todo_repository.dart (KONTRAK) ===
abstract class TodoRepository {
Future<List<Todo>> getTodos();
Future<void> addTodo(Todo todo);
}
// === usecases/get_todos.dart ===
class GetTodos {
final TodoRepository repo;
GetTodos(this.repo);
Future<List<Todo>> call() => repo.getTodos();
}
// === usecases/add_todo.dart ===
class AddTodo {
final TodoRepository repo;
AddTodo(this.repo);
Future<void> call(Todo t) => repo.addTodo(t);
}
Implementasi nyata: Model, DataSource, Repository Impl, lalu daftarkan ke locator.
Sekarang kita memenuhi kontrak yang dibuat di slide sebelumnya. Empat bagian:
TodoModel — anak dari Entity Todo, tapi ditambah fromJson untuk mengubah JSON dari API jadi objek Dart.TodoRemoteDataSource — yang benar-benar memanggil API (GET /todos) lalu memetakan tiap item jadi TodoModel.TodoRepositoryImpl — implements kontrak Domain. Ia menjembatani UseCase ke DataSource.setupTodo() — mendaftarkan semua objek di atas ke get_it (DI), sehingga bisa diambil di mana saja.Urutan daftar DI penting: yang dibutuhkan didaftarkan lebih dulu (DataSource → Repository → UseCase).
⏭️ Slide berikutnya: lapisan UI yang memakai semua ini.
// === models/todo_model.dart ===
class TodoModel extends Todo {
const TodoModel({required String id, required String title, bool done = false})
: super(id: id, title: title, done: done);
factory TodoModel.fromJson(Map<String, dynamic> j) => TodoModel(
id: j['id'], title: j['title'], done: j['done'] ?? false);
}
// === datasources/todo_remote_ds.dart ===
class TodoRemoteDataSource {
final ApiClient api;
TodoRemoteDataSource(this.api);
Future<List<TodoModel>> fetch() async {
final list = await api.get('/todos') as List;
return list.map((e) => TodoModel.fromJson(e)).toList();
}
}
// === repositories/todo_repository_impl.dart ===
class TodoRepositoryImpl implements TodoRepository {
final TodoRemoteDataSource ds;
TodoRepositoryImpl(this.ds);
@override
Future<List<Todo>> getTodos() => ds.fetch();
@override
Future<void> addTodo(Todo t) async {/* api.post */}
}
// === core/di/locator.dart ===
void setupTodo() {
sl.registerLazySingleton(() => TodoRemoteDataSource(sl()));
sl.registerLazySingleton<TodoRepository>(() => TodoRepositoryImpl(sl()));
sl.registerFactory(() => GetTodos(sl()));
sl.registerFactory(() => AddTodo(sl()));
}
Lapisan terluar: state notifier + halaman yang menampilkan daftar todo.
Lapisan terakhir yang dilihat user. Ini menyambungkan semuanya:
TodoNotifier — penyimpan state (loading + daftar todo). Ia memanggil UseCase getTodos(), lalu notifyListeners() agar UI ikut diperbarui.TodoPage — Widget yang watch notifier. Tampilkan spinner saat loading, atau daftar CheckboxListTile saat data siap.main() — merangkai semua: jalankan DI, lalu suntik TodoNotifier(sl()) via Provider.Lihat alurnya menyatu: UI → Notifier → UseCase → Repository → DataSource → API. Persis flowchart slide 8!
// === presentation/state/todo_notifier.dart ===
class TodoNotifier extends ChangeNotifier {
final GetTodos getTodos;
TodoNotifier(this.getTodos);
List<Todo> todos = [];
bool loading = false;
Future<void> load() async {
loading = true; notifyListeners();
todos = await getTodos(); // panggil UseCase
loading = false; notifyListeners();
}
}
// === presentation/pages/todo_page.dart ===
class TodoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final n = context.watch<TodoNotifier>();
if (n.loading) return const CircularProgressIndicator();
return ListView(
children: n.todos.map((t) => CheckboxListTile(
title: Text(t.title),
value: t.done,
onChanged: (_) {},
)).toList(),
);
}
}
// === main.dart — rangkai semua ===
void main() {
setupLocator(); setupTodo();
runApp(ChangeNotifierProvider(
create: (_) => TodoNotifier(sl())..load(),
child: const MyApp(),
));
}
UI hanya tahu TodoNotifier & GetTodos — buta total terhadap Dio, JSON, atau API. Itulah Clean Architecture yang utuh.
Baca potongan kode berikut, lalu tentukan ia milik layer yang mana. Klik jawaban yang benar.
class ProductModel extends Product {
factory ProductModel.fromJson(Map<String,dynamic> j)
=> ProductModel(id: j['id'], name: j['name']);
}
Petunjuk: perhatikan kata kunci Model dan fromJson.
Isi bagian kosong agar ProductRepository & implementasinya lengkap. Ketik di kotak lalu klik Periksa.
// Lengkapi tiga bagian kosong di bawah ini:
_____ class ProductRepository { // (1) kata kunci kontrak
Future<List<Product>> getProducts();
}
class ProductRepositoryImpl _____ ProductRepository { // (2)
final ApiClient api;
ProductRepositoryImpl(this.api);
@override
Future<List<Product>> getProducts() async {
final json = await api.get('/products');
return json.map((e) => ProductModel._____(e)).toList(); // (3)
}
}
Bangun struktur folder + skeleton code untuk fitur "Profil Pengguna" memakai Clean Architecture.
Kerjakan di proyek Flutter nyata. Gunakan slide 14–19 sebagai referensi pola.
Klik untuk menandai progresmu sendiri:
Semua yang telah kamu pelajari, dalam satu pandangan.
Presentation · Domain · Data
Satu pintu akses data
Membungkus kemampuan teknis
Ambil objek siap pakai (get_it)
core · features · shared
Todo Manager utuh
Selanjutnya di BAB 7: Testing & CI/CD — menjaga aplikasi enterprise-mu tetap andal.