Skip to content

Bab 22 — Arsitektur Berlapis dengan BLoC

Bayangkan sebuah tubuh manusia di mana setiap orang bebas memilih sendiri di mana menaruh jantung, paru-paru, dan ginjalnya. Tidak ada yang akan bertahan hidup. Yang membuat tubuh bekerja bukan sekadar keberadaan organnya, melainkan struktur yang menempatkan setiap organ di tempat yang benar.

Kode juga begitu. Arsitektur adalah kerangka yang menjaga semuanya tetap terorganisasi, stabil, dan mudah diuji.

BLoC bukan sekadar pustaka state management — ia juga sebuah pola arsitektur. Bab ini membahas bagaimana menyusun seluruh aplikasi di sekitarnya.

Tiga lapisan

┌──────────────────────────────────────────────────────────────┐
│  PRESENTATION LAYER                                          │
│  Widget, screen, komponen UI                                 │
│                                                              │
│  Tugas: menampilkan state, mengirim event                    │
│  Tidak boleh: memanggil API, mengurai JSON, menyimpan ke DB  │
└────────────────────────────┬─────────────────────────────────┘
                             │  event ↓   state ↑
┌────────────────────────────▼─────────────────────────────────┐
│  BUSINESS LOGIC LAYER                                        │
│  Bloc / Cubit                                                │
│                                                              │
│  Tugas: aturan bisnis, transisi keadaan, orkestrasi          │
│  Tidak boleh: tahu apa pun tentang widget                    │
└────────────────────────────┬─────────────────────────────────┘
                             │  panggil ↓   data ↑
┌────────────────────────────▼─────────────────────────────────┐
│  DATA LAYER                                                  │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  Repositories — menggabungkan & menyiapkan data         │ │
│  ├─────────────────────────────────────────────────────────┤ │
│  │  Data Providers — bicara dengan API, DB, cache          │ │
│  ├─────────────────────────────────────────────────────────┤ │
│  │  Models — bentuk data                                   │ │
│  └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘

Aturan yang mengikat semuanya: ketergantungan hanya boleh mengarah ke bawah. Presentation tahu tentang business logic; business logic tahu tentang data. Tidak pernah sebaliknya.

Kita akan membangunnya dari bawah ke atas, karena itulah urutan yang paling masuk akal ketika memulai aplikasi baru.

Lapisan data

Models

Kelas data murni. Tidak ada logika, tidak ada panggilan jaringan, tidak ada widget.

dart
// lib/data/models/weather.dart
import 'package:equatable/equatable.dart';

class Weather extends Equatable {
  const Weather({
    required this.kota,
    required this.suhu,
    required this.terasaSeperti,
    required this.deskripsi,
    required this.ikon,
    required this.kelembapan,
    required this.kecepatanAngin,
  });

  final String kota;
  final double suhu;
  final double terasaSeperti;
  final String deskripsi;
  final String ikon;
  final int kelembapan;
  final double kecepatanAngin;

  factory Weather.dariJson(Map<String, dynamic> json) {
    return Weather(
      kota: json['name'] as String,
      suhu: (json['main']['temp'] as num).toDouble(),
      terasaSeperti: (json['main']['feels_like'] as num).toDouble(),
      deskripsi: json['weather'][0]['description'] as String,
      ikon: json['weather'][0]['icon'] as String,
      kelembapan: (json['main']['humidity'] as num).toInt(),
      kecepatanAngin: (json['wind']['speed'] as num).toDouble(),
    );
  }

  String get urlIkon => 'https://openweathermap.org/img/wn/$ikon@2x.png';

  @override
  List<Object?> get props => [
        kota,
        suhu,
        terasaSeperti,
        deskripsi,
        ikon,
        kelembapan,
        kecepatanAngin,
      ];
}

Perhatikan extends Equatable dan props yang lengkap. Ini penting karena model akan menjadi bagian dari state, dan perbandingan state bergantung padanya — alasan lengkapnya di Bab 20.

Data Providers

Lapisan yang benar-benar bicara dengan dunia luar. Tugasnya sesempit mungkin: ambil data mentah, kembalikan dalam bentuk yang bisa dipakai. Tidak ada aturan bisnis di sini.

dart
// lib/data/data_providers/weather_api.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;

class KesalahanApi implements Exception {
  KesalahanApi(this.pesan);
  final String pesan;

  @override
  String toString() => pesan;
}

class WeatherApi {
  WeatherApi({http.Client? klien, required this.kunciApi})
      : _klien = klien ?? http.Client();

  final http.Client _klien;
  final String kunciApi;

  static const _host = 'api.openweathermap.org';
  static const _batasWaktu = Duration(seconds: 10);

  Future<Map<String, dynamic>> ambilCuacaMentah(String kota) async {
    final url = Uri.https(_host, '/data/2.5/weather', {
      'q': kota,
      'appid': kunciApi,
      'units': 'metric',
      'lang': 'id',
    });

    try {
      final respons = await _klien.get(url).timeout(_batasWaktu);

      if (respons.statusCode == 404) {
        throw KesalahanApi('Kota "$kota" tidak ditemukan.');
      }

      if (respons.statusCode >= 400) {
        throw KesalahanApi('Server bermasalah (${respons.statusCode}).');
      }

      return json.decode(respons.body) as Map<String, dynamic>;
    } on SocketException {
      throw KesalahanApi('Tidak ada koneksi internet.');
    } on TimeoutException {
      throw KesalahanApi('Server terlalu lama merespons.');
    } on FormatException {
      throw KesalahanApi('Format data tidak dikenali.');
    }
  }

  void tutup() => _klien.close();
}

Perhatikan bahwa WeatherApi mengembalikan Map<String, dynamic> mentah, bukan objek Weather. Ia tidak tahu apa-apa tentang model — pemisahan yang membuatnya sangat mudah diuji.

http.Client disuntikkan lewat konstruktor supaya bisa diganti dengan klien palsu saat pengujian. Teknik ini dibahas di Bab 26.

Repositories

Inilah satu-satunya bagian dari lapisan data yang boleh diakses BLoC. Tugasnya menggabungkan satu atau beberapa data provider, mengubah data mentah menjadi model, dan menerapkan strategi seperti cache.

dart
// lib/data/repositories/weather_repository.dart
import '../data_providers/weather_api.dart';
import '../models/weather.dart';

class WeatherRepository {
  WeatherRepository(this._api);

  final WeatherApi _api;

  // Cache sederhana di memori
  final Map<String, _EntriCache> _cache = {};
  static const _umurCache = Duration(minutes: 10);

  Future<Weather> ambilCuaca(String kota) async {
    final kunci = kota.toLowerCase().trim();

    // Kembalikan dari cache kalau masih segar
    final tersimpan = _cache[kunci];
    if (tersimpan != null && !tersimpan.kedaluwarsa) {
      return tersimpan.data;
    }

    final mentah = await _api.ambilCuacaMentah(kota);
    final cuaca = Weather.dariJson(mentah);

    _cache[kunci] = _EntriCache(cuaca);
    return cuaca;
  }

  void bersihkanCache() => _cache.clear();
}

class _EntriCache {
  _EntriCache(this.data) : _disimpanPada = DateTime.now();

  final Weather data;
  final DateTime _disimpanPada;

  bool get kedaluwarsa =>
      DateTime.now().difference(_disimpanPada) >
      WeatherRepository._umurCache;
}

Repository adalah tempat yang tepat untuk keputusan seperti:

  • Ambil dari cache dulu, jaringan kalau perlu.
  • Gabungkan data dari dua API berbeda.
  • Simpan ke basis data lokal setelah mengambil dari jaringan.
  • Kembalikan data offline kalau jaringan gagal.

BLoC tidak perlu tahu apa pun tentang keputusan itu. Ia hanya memanggil ambilCuaca(kota) dan menerima hasilnya.

Kenapa dua lapisan, bukan satu

Godaan untuk menggabungkan data provider dan repository menjadi satu kelas itu kuat, terutama di awal. Tetapi pemisahannya terbayar ketika:

  • Kamu menambahkan sumber kedua (cache lokal) — hanya repository yang berubah.
  • Kamu mengganti API — hanya data provider yang berubah.
  • Kamu menguji repository — data provider mudah dipalsukan.

Lapisan business logic

BLoC memanggil repository dan mengubah hasilnya menjadi state.

dart
// lib/logic/cubit/weather_state.dart
import 'package:equatable/equatable.dart';
import '../../data/models/weather.dart';

sealed class WeatherState extends Equatable {
  const WeatherState();

  @override
  List<Object?> get props => [];
}

final class WeatherInitial extends WeatherState {
  const WeatherInitial();
}

final class WeatherLoading extends WeatherState {
  const WeatherLoading();
}

final class WeatherLoaded extends WeatherState {
  const WeatherLoaded(this.cuaca);
  final Weather cuaca;

  @override
  List<Object?> get props => [cuaca];
}

final class WeatherError extends WeatherState {
  const WeatherError(this.pesan);
  final String pesan;

  @override
  List<Object?> get props => [pesan];
}
dart
// lib/logic/cubit/weather_cubit.dart
import 'package:bloc/bloc.dart';
import '../../data/data_providers/weather_api.dart';
import '../../data/repositories/weather_repository.dart';
import 'weather_state.dart';

class WeatherCubit extends Cubit<WeatherState> {
  WeatherCubit(this._repository) : super(const WeatherInitial());

  final WeatherRepository _repository;

  Future<void> ambilCuaca(String kota) async {
    if (kota.trim().isEmpty) {
      emit(const WeatherError('Masukkan nama kota terlebih dahulu.'));
      return;
    }

    emit(const WeatherLoading());

    try {
      final cuaca = await _repository.ambilCuaca(kota);
      emit(WeatherLoaded(cuaca));
    } on KesalahanApi catch (e) {
      emit(WeatherError(e.pesan));
    } catch (_) {
      emit(const WeatherError('Terjadi kesalahan tak terduga.'));
    }
  }

  Future<void> segarkan() async {
    final sekarang = state;
    if (sekarang is! WeatherLoaded) return;

    _repository.bersihkanCache();
    await ambilCuaca(sekarang.cuaca.kota);
  }

  void reset() => emit(const WeatherInitial());
}

Perhatikan bahwa validasi "kota tidak boleh kosong" berada di BLoC, bukan di widget. Ini aturan bisnis, dan menaruhnya di sini berarti ia berlaku dari mana pun ambilCuaca dipanggil — dan bisa diuji tanpa membangun satu widget pun.

BLoC tidak boleh mengimpor Flutter

Ini aturan yang layak ditegakkan dengan ketat:

dart
// ❌ Jangan pernah ada di dalam Bloc/Cubit
import 'package:flutter/material.dart';

emit(WeatherLoaded(cuaca, warna: Colors.blue));   // warna itu urusan UI
Navigator.of(context).push(...);                  // navigasi itu urusan UI

Kalau BLoC-mu butuh package:flutter/material.dart, hampir pasti ada logika UI yang bocor ke sana. BLoC yang murni Dart bisa diuji tanpa flutter_test, jauh lebih cepat, dan bisa dipakai ulang di aplikasi lain.

Lapisan presentation

Widget hanya menampilkan state dan mengirim aksi.

dart
// lib/presentation/screens/weather_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../logic/cubit/weather_cubit.dart';
import '../../logic/cubit/weather_state.dart';
import '../widgets/weather_card.dart';

class WeatherScreen extends StatefulWidget {
  const WeatherScreen({super.key});

  @override
  State<WeatherScreen> createState() => _WeatherScreenState();
}

class _WeatherScreenState extends State<WeatherScreen> {
  final _pengendaliKota = TextEditingController();

  @override
  void dispose() {
    _pengendaliKota.dispose();
    super.dispose();
  }

  void _cari() {
    context.read<WeatherCubit>().ambilCuaca(_pengendaliKota.text);
    FocusScope.of(context).unfocus();   // tutup papan ketik
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cuaca')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _pengendaliKota,
                    textInputAction: TextInputAction.search,
                    onSubmitted: (_) => _cari(),
                    decoration: const InputDecoration(
                      hintText: 'Nama kota...',
                      prefixIcon: Icon(Icons.location_city),
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton.filled(
                  onPressed: _cari,
                  icon: const Icon(Icons.search),
                ),
              ],
            ),
          ),

          Expanded(
            child: BlocConsumer<WeatherCubit, WeatherState>(
              listenWhen: (_, sesudah) => sesudah is WeatherError,
              listener: (context, state) {
                if (state is WeatherError) {
                  ScaffoldMessenger.of(context)
                    ..clearSnackBars()
                    ..showSnackBar(SnackBar(content: Text(state.pesan)));
                }
              },
              builder: (context, state) {
                return switch (state) {
                  WeatherInitial() => const Center(
                      child: Text('Cari kota untuk melihat cuacanya.'),
                    ),
                  WeatherLoading() => const Center(
                      child: CircularProgressIndicator(),
                    ),
                  WeatherLoaded(cuaca: final c) => RefreshIndicator(
                      onRefresh: () =>
                          context.read<WeatherCubit>().segarkan(),
                      child: ListView(
                        children: [WeatherCard(cuaca: c)],
                      ),
                    ),
                  WeatherError(pesan: final p) => Center(
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          const Icon(Icons.error_outline, size: 48),
                          const SizedBox(height: 12),
                          Padding(
                            padding: const EdgeInsets.symmetric(horizontal: 32),
                            child: Text(p, textAlign: TextAlign.center),
                          ),
                          const SizedBox(height: 12),
                          ElevatedButton(
                            onPressed: _cari,
                            child: const Text('Coba Lagi'),
                          ),
                        ],
                      ),
                    ),
                };
              },
            ),
          ),
        ],
      ),
    );
  }
}

Merangkai semuanya

dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'data/data_providers/weather_api.dart';
import 'data/repositories/weather_repository.dart';
import 'logic/cubit/weather_cubit.dart';
import 'presentation/screens/weather_screen.dart';

const _kunciApi = String.fromEnvironment('OPENWEATHER_API_KEY');

void main() {
  runApp(const AplikasiCuaca());
}

class AplikasiCuaca extends StatelessWidget {
  const AplikasiCuaca({super.key});

  @override
  Widget build(BuildContext context) {
    // Lapisan data disediakan paling luar
    return RepositoryProvider<WeatherRepository>(
      create: (_) => WeatherRepository(
        WeatherApi(kunciApi: _kunciApi),
      ),

      // Lapisan business logic mengambil dari lapisan data
      child: BlocProvider<WeatherCubit>(
        create: (context) => WeatherCubit(
          context.read<WeatherRepository>(),
        ),

        // Lapisan presentation
        child: MaterialApp(
          title: 'Cuaca',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlue),
            useMaterial3: true,
          ),
          home: const WeatherScreen(),
        ),
      ),
    );
  }
}

Urutan penyediaannya mencerminkan arah ketergantungan: data di luar, business logic di tengah, presentation di dalam.

Aliran data lengkap

Perhatikan bahwa setiap panah hanya melintasi satu batas lapisan. Widget tidak pernah bicara langsung ke API; Cubit tidak pernah bicara langsung ke http.

Struktur folder lengkap

lib/
├── main.dart

├── data/
│   ├── models/
│   │   ├── weather.dart
│   │   └── city.dart
│   ├── data_providers/
│   │   ├── weather_api.dart          ← HTTP
│   │   └── weather_local_storage.dart ← SharedPreferences / SQLite
│   └── repositories/
│       └── weather_repository.dart    ← menggabungkan keduanya

├── logic/
│   ├── cubit/
│   │   ├── weather_cubit.dart
│   │   ├── weather_state.dart
│   │   ├── settings_cubit.dart
│   │   └── settings_state.dart
│   └── bloc/
│       ├── search_bloc.dart
│       ├── search_event.dart
│       └── search_state.dart

├── presentation/
│   ├── screens/
│   │   ├── weather_screen.dart
│   │   ├── settings_screen.dart
│   │   └── search_screen.dart
│   ├── widgets/
│   │   ├── weather_card.dart
│   │   └── error_view.dart
│   └── router/
│       └── app_router.dart

└── constants/
    └── enums.dart

Menambahkan sumber kedua

Kekuatan arsitektur ini terlihat ketika kebutuhan berubah. Misalnya kamu ingin aplikasi tetap menampilkan cuaca terakhir ketika offline.

Yang berubah hanya repository. Data provider baru ditambahkan, tetapi Cubit dan seluruh UI tidak tersentuh sama sekali.

dart
// lib/data/data_providers/weather_local_storage.dart
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/weather.dart';

class WeatherLocalStorage {
  static const _kunci = 'cuaca_terakhir';

  Future<void> simpan(Weather cuaca) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(
      _kunci,
      json.encode({
        'name': cuaca.kota,
        'main': {
          'temp': cuaca.suhu,
          'feels_like': cuaca.terasaSeperti,
          'humidity': cuaca.kelembapan,
        },
        'weather': [
          {'description': cuaca.deskripsi, 'icon': cuaca.ikon}
        ],
        'wind': {'speed': cuaca.kecepatanAngin},
      }),
    );
  }

  Future<Weather?> muat() async {
    final prefs = await SharedPreferences.getInstance();
    final teks = prefs.getString(_kunci);
    if (teks == null) return null;

    try {
      return Weather.dariJson(json.decode(teks) as Map<String, dynamic>);
    } catch (_) {
      return null;
    }
  }
}
dart
// lib/data/repositories/weather_repository.dart — versi yang diperluas
class WeatherRepository {
  WeatherRepository(this._api, this._lokal);

  final WeatherApi _api;
  final WeatherLocalStorage _lokal;

  Future<Weather> ambilCuaca(String kota) async {
    try {
      final mentah = await _api.ambilCuacaMentah(kota);
      final cuaca = Weather.dariJson(mentah);

      // Simpan untuk dipakai saat offline
      await _lokal.simpan(cuaca);
      return cuaca;
    } on KesalahanApi {
      // Jaringan gagal — coba data tersimpan
      final tersimpan = await _lokal.muat();
      if (tersimpan != null) return tersimpan;

      rethrow;   // tidak ada cadangan, teruskan errornya
    }
  }
}

Satu berkas baru dan satu berkas diubah. WeatherCubit, WeatherScreen, dan seluruh widget tidak berubah satu baris pun. Itulah imbalan dari pemisahan lapisan.

Kesalahan arsitektur yang umum

BLoC memanggil HTTP langsung.

dart
// ❌ Melewati lapisan data
class WeatherCubit extends Cubit<WeatherState> {
  Future<void> ambil(String kota) async {
    final respons = await http.get(Uri.parse('...'));   // 💥
  }
}

Widget memanggil repository langsung.

dart
// ❌ Melewati lapisan business logic
onPressed: () async {
  final cuaca = await context.read<WeatherRepository>().ambilCuaca('Bandung');
  setState(() => _cuaca = cuaca);
}

Model mengandung logika UI.

dart
// ❌ Model tidak boleh tahu tentang warna atau widget
class Weather {
  Color get warnaLatar => suhu > 30 ? Colors.red : Colors.blue;
}

Pindahkan itu ke widget atau ke fungsi pembantu di lapisan presentation.

Satu BLoC raksasa untuk seluruh aplikasi.

dart
// ❌ Setiap perubahan kecil membangun ulang semuanya
class AppBloc extends Bloc<AppEvent, AppState> {
  // auth + cuaca + pengaturan + keranjang + notifikasi...
}

Pecah per fitur. Satu BLoC untuk satu tanggung jawab.

Latihan Mandiri

Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.

Variasi 1: Refaktor CuacaCubit — ⭐⭐ · 45–60 menit

Tantangan: Kode di bawah ini bekerja, tetapi seluruhnya rata dalam satu kelas: HTTP, penguraian JSON, aturan bisnis, dan penyimpanan bercampur di dalam Cubit. Refaktor menjadi tiga lapisan yang benar, tanpa mengubah perilakunya sama sekali.

dart
class CuacaCubit extends Cubit<CuacaState> {
  CuacaCubit() : super(CuacaAwal());

  Future<void> muat(String kota) async {
    emit(CuacaMemuat());
    try {
      final res = await http.get(
        Uri.parse('https://api.contoh.id/cuaca?kota=$kota'),
      );
      if (res.statusCode != 200) {
        emit(const CuacaGagal('Server bermasalah'));
        return;
      }
      final json = jsonDecode(res.body) as Map<String, dynamic>;
      final suhu = (json['temp_c'] as num).toDouble();
      final kelembapan = (json['humidity'] as num).toInt();

      // aturan bisnis: peringatan cuaca ekstrem
      final ekstrem = suhu > 38 || (suhu > 33 && kelembapan > 80);

      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('kota_terakhir', kota);

      emit(CuacaSiap(
        suhu: suhu,
        kelembapan: kelembapan,
        peringatan: ekstrem
            ? const Text('Cuaca ekstrem', style: TextStyle(color: Colors.red))
            : null,
      ));
    } catch (e) {
      emit(CuacaGagal(e.toString()));
    }
  }
}

Kriteria selesai:

  • Ada CuacaApi (data provider) yang hanya bicara HTTP dan mengembalikan data mentah, dan CuacaRepository yang mengubahnya menjadi model serta memutuskan urusan penyimpanan.
  • Cubit hanya menerima repository lewat konstruktor, memanggil satu metode, dan mengeluarkan state. Ia tidak mengimpor http maupun shared_preferences.
  • Cubit tidak mengimpor package:flutter/material.dart — perhatikan properti peringatan pada kode di atas dan pikirkan apa yang seharusnya ada di sana.
  • Aturan cuaca ekstrem tetap ada di lapisan yang benar, dan kamu bisa menjelaskan kenapa itu bukan pekerjaan widget maupun repository.
  • e.toString() yang bocor ke pengguna diganti pesan yang bisa ditindaklanjuti.

Petunjuk: Petunjuk terkuat ada di baris impor. Kalau Cubit-mu masih membutuhkan material.dart, berarti ada keputusan tampilan yang masih tinggal di sana — dan di kode ini keputusan itu terlihat jelas: sebuah Text berwarna merah yang dikirim sebagai bagian dari state. State seharusnya menyatakan fakta ("cuacanya ekstrem"), dan widget yang memutuskan bahwa fakta itu digambar merah. Untuk memisahkan api dari repository, tanyakan pada setiap baris: apakah baris ini akan berubah kalau kita mengganti penyedia API? Kalau ya, ia milik api. Apakah ia akan berubah kalau kita menambah cache? Kalau ya, ia milik repository.

Variasi 2: Sumber Kedua untuk Jadwal Bus — ⭐⭐⭐ · 45–60 menit

Tantangan: Bangun fitur jadwal bus berlapis tiga yang mengambil data dari API. Setelah bekerja, tambahkan sumber kedua: cache lokal yang dipakai ketika jaringan gagal. Syaratnya, penambahan itu tidak boleh menyentuh satu baris pun di BLoC maupun di widget.

Kriteria selesai:

  • Perubahan hanya terjadi di dalam repository dan satu data provider baru.
  • git diff membuktikan berkas BLoC dan widget tidak berubah sama sekali.
  • State membedakan data segar dari data cache, dan UI menampilkan penandanya.
  • Cache yang kedaluwarsa lebih dari batas tertentu tidak dipakai.

Petunjuk: Kriteria kedua adalah ujian arsitektur yang paling jujur dan paling cepat — kalau git diff menunjukkan BLoC ikut berubah, berarti ada keputusan tentang sumber data yang bocor ke sana. Perhatikan kriteria ketiga: penanda "ini data cache" adalah fakta tentang data, bukan keputusan tampilan, jadi ia memang pantas ada di dalam state. Bedakan itu dari warna atau ikon penandanya, yang tetap milik widget.

Variasi 3: Audit Arsitektur — ⭐⭐⭐⭐ · 60–90 menit

Tantangan: Ambil proyek Flutter mana pun yang sudah kamu tulis, lalu audit arsitekturnya terhadap seluruh aturan bab ini. Buat daftar temuan, urutkan menurut dampaknya, lalu perbaiki tiga yang paling berdampak.

Kriteria selesai:

  • Kamu menjalankan pencarian nyata di kode: material.dart di lapisan logika, http di luar data provider, dan data provider yang diakses langsung oleh BLoC.
  • Setiap temuan dicatat dengan lokasi berkasnya dan satu kalimat dampak.
  • Tiga temuan teratas diperbaiki tanpa mengubah perilaku aplikasi.
  • Ada satu temuan yang kamu putuskan tidak diperbaiki, disertai alasan yang bisa dipertahankan.

Petunjuk: Kriteria terakhir sengaja ada. Arsitektur adalah pertukaran, bukan daftar aturan yang harus dipenuhi seratus persen — sebuah BLoC kecil yang memanggil satu API sederhana tidak selalu pantas dipecah menjadi tiga berkas. Kemampuan menyatakan "aturan ini tidak sepadan di sini, karena..." adalah tanda kamu memahami alasannya, bukan sekadar menghafal bentuknya. Bab Bab 33 membahas titik itu lebih jauh.

Ikhtisar

  • Arsitektur BLoC punya tiga lapisan: presentation, business logic, dan data. Ketergantungan hanya mengarah ke bawah.
  • Lapisan data terbagi tiga: models (bentuk data), data providers (bicara dengan API/DB), dan repositories (menggabungkan & menyiapkan).
  • Hanya repository yang boleh diakses BLoC. Data provider tersembunyi di belakangnya.
  • Data provider mengembalikan data mentah; repository yang mengubahnya menjadi model.
  • Repository adalah tempat keputusan seperti cache, gabungan sumber, dan cadangan offline.
  • BLoC tidak boleh mengimpor package:flutter/material.dart. Kalau butuh, ada logika UI yang bocor ke sana.
  • Aturan bisnis (validasi, keputusan) berada di BLoC, bukan di widget.
  • Model tidak boleh mengandung logika UI seperti warna atau widget.
  • Sediakan lapisan dari luar ke dalam: RepositoryProviderBlocProviderMaterialApp.
  • Kekuatan arsitektur ini terlihat saat kebutuhan berubah: menambah sumber data kedua hanya mengubah repository, tanpa menyentuh BLoC maupun UI.
  • Pecah BLoC per fitur. Satu BLoC raksasa membuat setiap perubahan kecil membangun ulang semuanya.

Berikutnya: Bab 23 — Akses BLoC & Navigasi.

Transkrip asli

Disintesis dari 2_flutter_bloc-zero-to-hero/4_bloc-architecture.md (analogi kerangka tubuh, tiga lapisan, pembagian data layer menjadi models / data providers / repositories). Lihat PDF BLoC Zero to Hero.

Rangkuman pembelajaran pribadi, disusun ulang dari beberapa kursus Flutter.