Week 7

Clean Architecture

Pemrograman Mobile

Flutter • SOLID • Feature-First • Repository • Use Case • DI

Dari aplikasi yang jalan → aplikasi yang terstruktur profesional

Learning Outcomes

Setelah mengikuti pertemuan ini, mahasiswa mampu:

  1. Menjelaskan SOLID dan separation of concerns dengan contoh Flutter.
  2. Membedakan struktur feature-first vs layer-first.
  3. Menjelaskan 3 layer dan aturan dependensi (mengarah ke dalam).
  4. Membedakan entity vs model, repository, use case, dan DI.
  5. Merefactor project Minggu 5/6 menjadi feature-first.
  6. Menguji use case dengan repository palsu.
  7. Menilai usulan AI dan mempertahankannya.

Mengapa Arsitektur?

Project Week 6 jalan, tapi:

Widget 400 baris: fetch API + parsing + format tanggal
Notifier memanggil Dio / SQLite langsung
Tambah 1 fitur → edit 5 file yang sudah jalan
Test butuh database & jaringan sungguhan

Project nyata butuh struktur:

presentation → domain ← data
logika bisnis murni → bisa diunit-test
satu fitur → satu folder, hapus tanpa takut

Tujuan: bukan sekadar pindah folder, tapi arah panah dependensi.

Mini Poll

Widget menampilkan daftar catatan dan langsung memanggil sqflite...

Prinsip apa yang dilanggar?

A. Single Responsibility
B. Dependency Inversion
C. Keduanya
D. Tidak ada, itu efisien

Diskusi

Apa beda "rapi folder" dengan "bersih arsitektur"?

Bagian 1

SOLID & Tiga Layer

SOLID Sekilas (1/2)

Prinsip Arti praktis
Single Responsibility satu kelas, satu alasan berubah
Open/Closed tambah perilaku via ekstensi
Liskov Substitution pengganti tak merusak aslinya

Gejala di Flutter:

S → widget 400 baris: API + parsing + format
O → sumber data baru = edit widget lama
L → repo palsu melempar error yang tak pernah ada

SOLID Sekilas (2/2)

Prinsip Arti praktis
Interface Segregation jangan paksa dependensi tak dipakai
Dependency Inversion bergantung pada abstraksi

Gejala di Flutter:

I → satu interface raksasa AppRepository
    untuk auth + notes + push sekaligus
D → notifier memanggil Dio / sqflite langsung
    alih-alih repository

D adalah fondasi seluruh minggu ini.

Aturan Dependensi

presentation (widget, notifier, router)
      |
      v  bergantung ke
domain (entity, repository interface,
        use case, failure)
      ^
      |  diimplementasikan oleh
data (model, repository impl,
      Dio, SQLite, secure storage)

Aturan emas:

Dependensi hanya mengarah ke dalam.
domain tak tahu Flutter, Dio, SQLite.

Diskusi

Mengapa domain harus murni Dart?

Feature-First vs Layer-First

Struktur Bentuk Kapan
feature-first features/notes/{data,domain,presentation} multi-fitur (pilihan kita)
layer-first lib/{data,domain,presentation} global project kecil satu fitur

Keuntungan feature-first:

satu fitur → dipahami dalam satu folder
hapus fitur → hapus satu folder, tanpa takut
tim → kerja paralel per fitur

Kosakata Peran

Entity     → objek bisnis murni (domain)
Model      → representasi JSON/SQLite + mapping (data)
Repository → interface di domain, impl di data
Use case   → satu operasi bisnis (domain)
DI         → wiring via provider Riverpod

Mapping (toMap/fromMap) hanya di model.
Widget tak pernah new Repository() sendiri.

Bagian 2

Audit Project Lama

Petakan File ke Layer

Isi di README sebelum refactor:

File Layer Masalah?
pages/home_page.dart presentation panggil Dio langsung?
data/api_client.dart data OK bila hanya dipakai repo
providers/auth_provider.dart presentation OK bila hanya panggil repo
data/auth_repository.dart data interface + impl tercampur?

Tanpa peta ini, refactor = pindah file buta.

Tiga Grep Pemburu Pelanggaran

1. Widget menyentuh data langsung:
   Dio( | openDatabase | FlutterSecureStorage
   di lib/pages, lib/widgets

2. Logika bisnis di build():
   DateFormat | jsonDecode | toIso8601String
   di lib/pages, lib/widgets

3. DI bocor (instansiasi manual):
   Repository( | Dio(BaseOptions
   di lib/pages, lib/providers

Target akhir: ketiga grep NOL hasil di presentation.

Struktur Target

lib/
├── core/
│   └── failures.dart
├── features/
│   └── notes/
│       ├── domain/
│       │   ├── entities/note.dart
│       │   ├── repositories/ (interface!)
│       │   └── usecases/
│       ├── data/
│       │   ├── models/
│       │   └── repositories/ (impl)
│       └── presentation/
│           ├── providers/ (notifier + DI)
│           └── pages/
└── routes.dart

Aturan main

Commit snapshot dulu. Refactor kode yang sudah jalan.

Bagian 3

Domain & Data

Entity Murni

Tanpa import Flutter, tanpa mapping:

class Note {
  const Note({
    this.id,
    required this.title,
    this.body = '',
    required this.updatedAt,
    this.dirty = false,
  });

  final int? id;
  final String title;
  final String body;
  final DateTime updatedAt;
  final bool dirty;
}

Entity = bahasa bisnis. Bukan bahasa database.

Failure + Kontrak (1/2)

Kegagalan eksplisit, bukan exception bocor:

sealed class Failure {
  const Failure(this.message);
  final String message;
}

class LocalFailure extends Failure {
  const LocalFailure(super.message);
}

class NetworkFailure extends Failure {
  const NetworkFailure(super.message);
}

Failure + Kontrak (2/2)

Kontrak tinggal di domain:

abstract class NoteRepository {
  Future<({List<Note> notes,
    Failure? failure})> fetchNotes();

  Future<({Note? note,
    Failure? failure})> addNote({
    required String title,
    String body = '',
  });
}

Alternatif industri: fpdart / dartz
(Either<Failure, T>). Pola kontraknya sama.

Model: Mapping Hanya Di Sini

class NoteModel extends Note {
  Map<String, Object?> toMap() => {
    'id': id,
    'updated_at':
        updatedAt.toIso8601String(),
    'dirty': dirty ? 1 : 0,
  };

  factory NoteModel.fromMap(map) {
    return NoteModel(
      id: (map['id'] as num?)?.toInt(),
      title: map['title'] as String? ?? '',
      updatedAt: DateTime.tryParse(
        map['updated_at'] as String? ?? '',
      ) ?? DateTime.fromMillisecondsSinceEpoch(0),
    );
  }

  Note toEntity() => Note(...);
}

Repository Impl

class NoteRepositoryImpl
    implements NoteRepository {
  NoteRepositoryImpl({
    required Future<Database>
      Function() openDb,
  }) : _openDb = openDb;

  @override
  Future<...> fetchNotes() async {
    try {
      final db = await _openDb();
      final rows = await db.query(
        'notes',
        orderBy: 'updated_at DESC');
      return (notes: rows
        .map((r) => NoteModel
          .fromMap(r).toEntity())
        .toList(), failure: null);
    } catch (e) {
      return (notes: const [],
        failure: LocalFailure('...$e'));
    }
  }
}

Konversi .toEntity() di batas data → domain.

Use Case: Satu Operasi Bisnis

class GetNotes {
  const GetNotes(this._repository);
  final NoteRepository _repository;

  Future<({List<Note> notes,
    Failure? failure})> call() {
    return _repository.fetchNotes();
  }
}

Kapan use case perlu?

perlu → gabung >1 repo / ada aturan bisnis
cukup repo langsung → CRUD satu-baris

Keputusan ini bagian AI Challenge.

Wiring DI dengan Riverpod

Bagian 4 - Presentation, DI & Verifikasi: satu-satunya tempat wiring:

final noteRepositoryProvider = Provider<NoteRepository>((ref) {
  return NoteRepositoryImpl(openDb: openNotesDb);
});
final getNotesProvider = Provider<GetNotes>((ref) {
  return GetNotes(ref.watch(noteRepositoryProvider));
});
final notesProvider = FutureProvider<List<Note>>((ref) async {
  final result = await ref.watch(getNotesProvider).call();
  if (result.failure != null) throw Exception(result.failure!.message);
  return result.notes;
});

Halaman Steril

class NotesPage
    extends ConsumerWidget {
  @override
  Widget build(context, ref) {
    final state =
        ref.watch(notesProvider);
    return state.when(
      loading: () => const Center(
        child:
          CircularProgressIndicator()),
      error: (e, _) => ErrorView(
        message: '$e',
        onRetry: () => ref.invalidate(
          notesProvider),
      ),
      data: (notes) =>
        NotesList(notes),
    );
  }
}

Tanpa SQL, Dio, JSON, format tanggal.

Verifikasi: Definisi Selesai

1. Presentation steril (NOL hasil):
   Dio( | openDatabase | jsonDecode ...

2. Domain steril (NOL hasil):
   package:flutter | package:dio |
   package:sqflite | package:firebase ...

3. flutter analyze bersih + test hijau

Refactor selesai bila:

app berjalan IDENTIK (uji 5 mnt/fitur)
+ grep nol + analyze bersih + test hijau

Anti-Pattern

Hindari:

1. Pindah folder tanpa putus dependensi

widget → Dio tetap ada = bukan Clean

2. Use case untuk tiap CRUD satu-baris

over-engineering → tim kecil terbebani

3. Model bocor ke UI

UI hanya kenal entity, tak pernah model

4. Refactor tanpa snapshot commit

rusak = tak bisa rollback & bandingkan

AI Lab

Gunakan AI sebagai Co-Developer

Contoh prompt:

Project: campus_notify (auth + FCM + pengumuman).
Kondisi: lib/{data, providers, pages},
repo tercampur, widget panggil Dio langsung.
Tugas:
1. Usulkan feature-first Clean Architecture.
2. Tiap file lama → tujuan baru.
3. Tandai over-engineering untuk CRUD sederhana.
4. Wiring DI Riverpod tanpa package tambahan.
Jelaskan trade-off tiap keputusan.

Jangan berhenti pada output AI.

AI Verification Challenge

Setelah AI mengusulkan:

  1. Baca — pahami arah dependensi usulan.
  2. Verifikasi — interface di domain? grep, bukan sekilas.
  3. Tolak/terima — use case per CRUD? tolak + alasan.
  4. Uji — 3 grep sterilitas + test hijau.
  5. Refactor — rapikan, sederhanakan yang berlebih.
  6. Jelaskan — gambar panah tanpa catatan.

Uji minimal

  • entity bebas mapping
  • fake repo untuk test domain
  • before/after screenshot identik
  • flutter analyze + flutter test

Aktivitas Kelompok

Code Review 15 Menit

Cari minimal:

  • 2 pelanggaran dependensi (widget → Dio/SQLite)
  • 2 masalah kontrak (interface di data, model bocor)
  • 1 over-engineering (use case tak perlu)
  • 1 masalah DI (new Repository di widget)
  • 1 improvement dari usulan AI

Kemudian jawab:

Apakah struktur ini layak production? Mengapa?

Quiz Cepat

1. Interface repository tinggal di?

A. Data
B. Domain
C. Presentation
D. Core

2. Kapan use case dibutuhkan?

A. Selalu, untuk tiap CRUD
B. Saat gabung repo / ada aturan bisnis
C. Tidak pernah
D. Hanya untuk login

3. Domain boleh import?

A. Flutter + Dio
B. sqflite + Firebase
C. Pure Dart saja
D. Apa saja

Jawaban Quiz

1 → B

Kontrak milik domain; data hanya mengimplementasikan.

2 → B

CRUD satu-baris → repo langsung cukup

3 → C

domain murni → bisa diunit-test murni

AI Challenge

Gunakan AI untuk usulan reorganisasi:

"Usulkan feature-first Clean Architecture untuk campus_notify + tandai over-engineering."

Kemudian mahasiswa wajib:

  1. memahami arah dependensi usulan
  2. menemukan minimal 2 kelemahan usulan AI
  3. membuktikan sterilitas dengan grep
  4. menyederhanakan yang berlebih
  5. mendokumentasikan alasan di docs/
  6. mencatat perubahan manual

Deliverables

Repository

week-07-clean-architecture/
├── lib/
├── test/
├── docs/
├── README.md
└── screenshots/

README minimal berisi:

  • diagram layer + arah dependensi
  • hasil 3 grep sterilitas
  • screenshot before/after identik
  • cara menjalankan
  • AI tools yang digunakan
  • prompt + tabel usulan-vs-keputusan
  • masalah ditemukan dan hasil testing

Rubrik Week 7

Komponen Bobot
Entity/Model + Repository I/Impl 20%
Use Case + DI + Dependency Rule 25%
Refactor Identik + Bukti Grep 15%
UI & UX 10%
Testing 10%
Code Quality 10%
Responsible AI Usage 10%
Total 100%

Checklist Sebelum Submit

Struktur

  • [ ] Interface di domain, impl di data
  • [ ] Mapping hanya di model
  • [ ] Widget tak new Repository sendiri

Bukti

  • [ ] 3 grep sterilitas nol hasil
  • [ ] Before/after identik + screenshot
  • [ ] 2 test lulus (sukses + failure)

AI

  • [ ] Prompt + tabel keputusan dicatat
  • [ ] Mampu gambar panah tanpa AI

Exit Ticket

Jawab sebelum kelas berakhir:

1. Apa perbedaan:

Entity
vs
Model

2. Kapan:

Use case perlu
vs
Repo langsung cukup

3. Mengapa interface di domain, bukan di data?

4. Bagian mana usulan AI yang Anda tolak, dan mengapa?

Key Takeaways

SOLID

Bergantung pada abstraksi, bukan concretion.

Layer

presentation → domain ← data. Selalu ke dalam.

Pragmatisme

Use case saat logika tumbuh, bukan untuk tiap CRUD.

AI

AI mengusulkan reorganisasi.
Developer menilai dan memutuskan.

Next Week

Week 8: Mid Project Review & Code Review

Campus Notify (terstruktur)
   ↓
Git Flow + Pull Request
   ↓
Review sejawat + static analysis
   ↓
Project siap paruh kedua semester

Target

Kode yang tidak hanya jalan dan rapi, tapi lolos review tim.

Build → Verify → Explain

Prinsip Pembelajaran Mobile

         AI
          ↓
       Generate
          ↓
       Developer
          ↓
         Verify
          ↓
         Test
          ↓
       Refactor
          ↓
        Explain

Jangan hanya bisa memindahkan file.

Jadilah developer yang memahami ke mana panah dependensi menunjuk.