BAB 06 · CLEAN ARCH
PRESENTATION
DOMAIN
DATA
CLEAN
ARCH
FLUTTER
BAB 6 — CLEAN ARCHITECTURE
Membangun Aplikasi Flutter Skala Enterprise
KELAS 11 SEMESTER 2 MOBILE DEVELOPMENT
Gunakan tombol ▶ atau tombol panah ← → untuk navigasi
Peta Perjalanan Belajar

Posisimu di Kurikulum

Kamu telah menempuh lima bab. Sekarang semua keping pengetahuan itu kita rangkai menjadi satu arsitektur yang rapi dan profesional.

1
REST API
2
JSON & Model
3
Authentication
4
State Mgmt
5
Provider/Riverpod
6
CLEAN ARCH
INTI BAB INI

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.

Konsep Dasar

Apa itu Clean Architecture?

Bayangkan sebuah rumah. Rumah yang baik dibangun berlapis: dari pondasi, lalu dinding, baru interior.

DOMAIN · Dinding
DATA · Pondasi

🏠 Rumah dibangun lapis demi lapis

Analogi Bangunan Rumah

  • Pondasi = Data Layer — tempat data berasal (API, database). Tersembunyi tapi menopang semuanya.
  • Dinding = Domain Layer — struktur & aturan rumah. Logika bisnis inti.
  • Atap & Interior = Presentation Layer — yang dilihat dan disentuh penghuni (UI).

Kalau kamu mau ganti cat dinding (UI), kamu tidak perlu membongkar pondasi. Itulah kekuatan pemisahan layer.

MaintainableTestableScalable
Overview

Tiga Lapisan Arsitektur

Data mengalir dari atas ke bawah saat meminta, lalu kembali ke atas saat membawa hasil. Setiap lapisan hanya bicara dengan tetangganya.

🖥️

Presentation Layer

Widget, Page, State (BLoC/Provider) — apa yang dilihat user

🧠

Domain Layer

UseCase, Entity, Repository Interface — aturan bisnis murni

📦

Data Layer

Repository Impl, DataSource, Model — sumber & penyimpanan data

Aturan emas: Presentation → Domain → Data. Lapisan dalam (Domain) TIDAK BOLEH tahu soal lapisan luar (UI/Data spesifik).

Layer 1 dari 3

Presentation Layer

Lapisan yang berurusan dengan tampilan dan interaksi user.

ANALOGI

🍽️ Pramusaji di Restoran

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.

login_page.dart
// 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.

Layer 2 dari 3

Domain Layer

Jantung aplikasi: berisi aturan bisnis murni, bebas dari Flutter maupun sumber data.

ANALOGI

👨‍🍳 Kepala Dapur

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).

login_usecase.dart
// 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.

Layer 3 dari 3

Data Layer

Lapisan yang benar-benar mengambil & menyimpan data dari API atau database lokal.

ANALOGI

🏗️ Gudang Bahan Makanan

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).

auth_repository_impl.dart
// 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.

Visualisasi

Alur Data End-to-End

Ikuti perjalanan satu tap tombol "Masuk" menembus seluruh lapisan, lalu kembali membawa hasil.

① UITap tombol Masuk
② UseCaseValidasi aturan
③ RepositoryPilih sumber data
④ API/DBAmbil data nyata
⑤ ResponseBalik ke UI

UI memanggil

context.read<AuthController>().login() — status: loading

②③

UseCase → Repository

LoginUseCase memanggil AuthRepository.login()

DataSource → API

POST /login → server membalas JSON

UI diperbarui

User object diterima — status: success → tampilkan Home

Pattern Inti #1

Repository Pattern

Pola yang menyembunyikan sumber data dari pengguna data.

ANALOGI

🏘️ Agen Properti

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.

Mengapa Penting?

  • Satu pintu masuk data — semua akses lewat repository.
  • Mudah ditukar — ganti API jadi mock saat testing tanpa ubah UI.
  • Domain tetap bersih — hanya tahu interface, bukan detail.
TANPA REPOSITORY

Widget langsung panggil Dio/http → kacau, sulit dites.

DENGAN REPOSITORY

Widget → UseCase → Repository → sumber. Rapi & fleksibel.

Pattern Inti #1 · Kode

Repository Pattern — Kode Lengkap

Satu interface, dua implementasi yang bisa ditukar: nyata vs mock.

user_repository.dart
// === 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();
  }
}
user_repository_mock.dart
// === 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.

Pattern Inti #2

Service Pattern

Membungkus sebuah kemampuan teknis (auth, notifikasi, lokasi) menjadi layanan yang rapi.

ANALOGI

🛵 Layanan Ojek Online

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.

services.dart
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.

Pattern Inti #3

Dependency Injection (DI)

Daripada setiap kelas membuat sendiri kebutuhannya, kita "suntikkan" dari luar.

ANALOGI

🏪 Toko Serba Ada

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.

Kenapa Tidak Buat Sendiri?

  • Tanpa DI: tiap kelas bikin sendiri Dio, Storage, Repo → duplikasi & sulit dites.
  • Dengan DI: satu tempat mendaftarkan semua, kelas tinggal mengambil.

Di Flutter, tools populer untuk DI:

get_itriverpod (Provider)injectable

DI membuat dependency menjadi terpusat, sekali daftar, pakai di mana saja.

Pattern Inti #3 · Kode

DI — Kode & Perbandingan

Lihat bedanya: membuat manual (kacau) vs mengambil dari locator (rapi).

locator.dart — setup get_it
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().

perbandingan.dart
// ❌ 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)),
);
Struktur Proyek

Folder Structure Enterprise

Pohon folder yang konsisten membuat proyek besar tetap mudah dinavigasi seluruh tim.

lib/
├── core/ # dipakai seluruh app
│ ├── error/ # Failure, Exception
│ ├── network/ # ApiClient, Dio
│ └── di/ # locator.dart
├── features/ # per fitur
│ ├── auth/
│ ├── profile/
│ └── todo/
├── shared/ # widget & util reusable
│ ├── widgets/
│ └── utils/
└── main.dart

Tiga Pilar Folder

  • core/ — fondasi teknis yang dipakai semua fitur: jaringan, error handling, DI.
  • features/ — setiap fitur berdiri sendiri lengkap dengan 3 layer-nya. Mudah ditambah/dihapus.
  • shared/ — komponen reusable (tombol, formatter) yang dipakai lintas fitur.

Pendekatan ini disebut feature-first: tiap fitur adalah modul mandiri. Cocok untuk tim besar dan aplikasi yang terus tumbuh.

Struktur Proyek · Detail

Zoom-in: Satu Feature

Setiap fitur memuat tiga layer Clean Architecture secara lengkap dan terpisah.

features/auth/
├── data/ # Data Layer 📦
│ ├── datasources/
│ │ └── auth_remote_ds.dart
│ ├── models/
│ │ └── user_model.dart
│ └── repositories/
│ └── auth_repo_impl.dart
├── domain/ # Domain Layer 🧠
│ ├── entities/ user.dart
│ ├── repositories/ auth_repo.dart
│ └── usecases/ login_uc.dart
└── presentation/ # UI Layer 🖥️
├── bloc/ auth_bloc.dart
├── pages/ login_page.dart
└── widgets/ login_form.dart

Membaca Strukturnya

Perhatikan ada folder repositories di DUA tempat:

  • domain/repositories/ → berisi abstract class (kontrak).
  • data/repositories/ → berisi implements (implementasi nyata).

Inilah penerapan fisik dari prinsip: Domain mendefinisikan kontrak, Data memenuhinya.

Pola folder ini diulang sama persis untuk setiap fitur → konsisten & mudah ditebak.

Mini Project

Studi Kasus: Todo Manager

Kita bangun fitur "Daftar Tugas" dengan Clean Architecture, dari layer terdalam ke terluar.

📦 ①

Data Layer

TodoModel, RemoteDataSource (ambil dari API), TodoRepositoryImpl

🧠 ②

Domain Layer

Entity Todo, TodoRepository (kontrak), GetTodos & AddTodo UseCase

🖥️ ③

Presentation

TodoBloc/Provider, TodoPage, TodoTile widget

Urutan membangun: Domain (definisi)Data (implementasi)Presentation (tampilan). Tiga slide berikut adalah kode lengkapnya.

TUJUAN BELAJAR

Setelah mengetik ulang kode 3 slide berikut, kamu akan punya satu fitur utuh yang menerapkan semua konsep BAB ini sekaligus.

Mini Project · Bagian 1/3

Domain Layer — Todo

Mulai dari inti: Entity, kontrak Repository, dan UseCase. Murni Dart.

📖 Penjelasan

Kita mulai dari Domain karena ia adalah inti yang tidak bergantung pada apa pun.

  • Entity Todo — objek bisnis murni. Hanya berisi data penting (id, title, done), tanpa fromJson atau Flutter.
  • TodoRepositoryabstract 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.

domain/ — entities, repository, usecases
// === 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);
}
Mini Project · Bagian 2/3

Data Layer + DI Setup

Implementasi nyata: Model, DataSource, Repository Impl, lalu daftarkan ke locator.

📖 Penjelasan

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.
  • TodoRepositoryImplimplements 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.

data/ + core/di/locator.dart
// === 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()));
}
Mini Project · Bagian 3/3

Presentation Layer

Lapisan terluar: state notifier + halaman yang menampilkan daftar todo.

📖 Penjelasan

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/ — provider + page
// === 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.

Latihan 1

Identifikasi Layer

Baca potongan kode berikut, lalu tentukan ia milik layer yang mana. Klik jawaban yang benar.

mystery.dart
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.

Latihan 2

Lengkapi Repository Pattern

Isi bagian kosong agar ProductRepository & implementasinya lengkap. Ketik di kotak lalu klik Periksa.

product_repository.dart
// 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)
  }
}
Tugas Akhir Bab

Mini Project Mandiri

Bangun struktur folder + skeleton code untuk fitur "Profil Pengguna" memakai Clean Architecture.

📋 Instruksi

  • Buat folder features/profile/ dengan 3 sub-layer (data, domain, presentation).
  • Domain: Entity Profile, kontrak ProfileRepository, UseCase GetProfile.
  • Data: ProfileModel.fromJson, RemoteDataSource, RepositoryImpl.
  • Presentation: ProfileNotifier + ProfilePage.
  • Daftarkan semuanya di locator.dart (DI).

Kerjakan di proyek Flutter nyata. Gunakan slide 14–19 sebagai referensi pola.

✅ Kriteria Penilaian

Klik untuk menandai progresmu sendiri:

Pemisahan 3 layer benar & tidak tercampur (30%)
Naming convention konsisten (Impl, UseCase, Model) (20%)
Domain tidak meng-import Flutter/Dio (20%)
DI setup di locator.dart berfungsi (20%)
Aplikasi run tanpa error & menampilkan data (10%)
Rangkuman & Penutup

Peta Konsep BAB 6

Semua yang telah kamu pelajari, dalam satu pandangan.

🏛️

3 Layer

Presentation · Domain · Data

🏘️

Repository

Satu pintu akses data

🛵

Service

Membungkus kemampuan teknis

🏪

Dependency Injection

Ambil objek siap pakai (get_it)

🌳

Folder Enterprise

core · features · shared

Mini Project

Todo Manager utuh

Selanjutnya di BAB 7: Testing & CI/CD — menjaga aplikasi enterprise-mu tetap andal.

Arsitektur rapi = aplikasi yang hidup lama.