Skip to main content

๐Ÿงฉ BLoC Architecture Guide

Guide lengkap untuk implementasi BLoC (Business Logic Component) pattern di MStore Mobile dengan proper scope dan lifecycle management.

๐Ÿ“Œ Prinsip Dasar

Siapa yang Memiliki State?

BLoC bukan sekadar โ€œcontrollerโ€ โ€” dia pemilik state dan event lifecycle.Pertanyaannya bukan โ€œdi mana paling mudahโ€, tapi siapa yang bertanggung jawab mengelola durasi hidup state itu.

โš™๏ธ Hierarki UI dan Scope yang Tepat

Tabel Scope BLoC

LevelContohCocok untuk BLoC?Alasan
App (root)main.dartโœ… Ya, tapi hanya global stateAuth, UserSession, Theme
Route / Feature/sales, /inventoryโœ… IDEALBLoC punya domain boundary jelas
Page / ScaffoldSalesListPageโš ๏ธ Bisa, jika state hanya untuk page ituHanya 1 page concern
Component / WidgetSalesCardWidgetโŒ TIDAKBLoC akan hidup-mati tiap rebuild

๐Ÿงฉ Best Practice BLoC Scope per Domain

Global / Persistent

Scope: Root (di MaterialApp)Contoh:
  • AuthBloc
  • SettingsBloc
  • ThemeBloc

Feature-based

Scope: RouteContoh:
  • SalesBloc
  • InventoryBloc
  • HRBloc

Ephemeral UI

Scope: Page / Widget StateContoh:
  • FilterCubit
  • TabIndexCubit
  • DialogCubit
Gunakan Cubit untuk state lokal kecil, bukan Bloc penuh.โ€œGunakan BLoC untuk domain state, Cubit untuk UI state.โ€

๐Ÿง  Struktur Ideal (Modular + Feature Scoped)

lib/
 โ”ฃ features/
 โ”ƒ โ”ฃ sales/
 โ”ƒ โ”ƒ โ”ฃ bloc/
 โ”ƒ โ”ƒ โ”ƒ โ”ฃ sales_bloc.dart
 โ”ƒ โ”ƒ โ”ƒ โ”ฃ sales_event.dart
 โ”ƒ โ”ƒ โ”ƒ โ”— sales_state.dart
 โ”ƒ โ”ƒ โ”ฃ pages/
 โ”ƒ โ”ƒ โ”ƒ โ”— sales_page.dart
 โ”ƒ โ”ƒ โ”ฃ widgets/
 โ”ƒ โ”ƒ โ”ƒ โ”— sales_card.dart
 โ”ƒ โ”ƒ โ”ฃ sales_repository.dart
 โ”ƒ โ”ƒ โ”— sales_service.dart
 โ”ƒ โ”ฃ inventory/
 โ”ƒ โ”ƒ โ”ฃ bloc/
 โ”ƒ โ”ƒ โ”ฃ pages/
 โ”ƒ โ”ƒ โ”— ...
 โ”ฃ cmd/
 โ”ƒ โ”— routes/
 โ”ƒ   โ”ฃ core/
 โ”ƒ   โ”ƒ โ”— auth_routes.dart  โ† BlocProvider here
 โ”ƒ   โ”ฃ ops/
 โ”ƒ   โ”ƒ โ”— sales_routes.dart  โ† BlocProvider here
 โ”ƒ   โ”— routes.dart
 โ”ฃ app.dart
 โ”— main.dart

๐Ÿ”ง Implementation Pattern

โœ… CORRECT: Route-Level BlocProvider

// cmd/routes/core/auth_routes.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';

final List<RouteBase> authRoutes = [
  GoRoute(
    path: '/auth/login',
    builder: (context, state) => BlocProvider(
      create: (_) => getIt<AuthBloc>(),  // โœ… Route-level
      child: const LoginPage(),
    ),
  ),
];
Benefits:
  • โœ… AuthBloc hanya hidup selama route aktif
  • โœ… Semua widget di bawah LoginPage bisa context.watch<AuthBloc>()
  • โœ… Tidak membanjiri global scope
  • โœ… Auto-disposed saat keluar route

โŒ ANTIPATTERN: Page-Level BlocProvider

// โŒ JANGAN SEPERTI INI
class SalesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => SalesBloc(),  // โŒ Recreate setiap rebuild
      child: Scaffold(...),
    );
  }
}
Problems:
  • โŒ Setiap kali route direpush, SalesBloc direcreate
  • โŒ Request API ulang
  • โŒ State hilang saat navigasi antar tab
  • โŒ Tidak bisa share state ke sub-page (/sales/detail/:id)

๐Ÿงฎ MultiBloc Pattern

Jika satu feature butuh beberapa state domain:
// cmd/routes/ops/sales_routes.dart
return MultiBlocProvider(
  providers: [
    BlocProvider(create: (_) => getIt<SalesBloc>()),
    BlocProvider(create: (_) => getIt<FilterCubit>()),
    BlocProvider(create: (_) => getIt<SortCubit>()),
  ],
  child: const SalesPage(),
);
Results:
  • โœ… Semua anak widget bisa akses multi-state
  • โœ… Masih terisolasi dalam feature route
  • โœ… Lifecycle otomatis dihapus saat keluar route

๐Ÿงฑ Decision Matrix

  • Kapan taruh di Route
  • Kapan taruh di Page
  • Kapan taruh di Component
  • State domain / API calls
  • Perlu diakses antar sub-page
  • Misal: Sales, Inventory, HR
  • Lifecycle: Lives as long as route lives

โœ… Implementation Checklist

1

Define BLoC Scope

Tentukan apakah state ini:
  • Global (auth, theme)
  • Feature-level (sales, inventory)
  • UI-only (filter, sort)
2

Create BLoC/Cubit

@injectable
class SalesBloc extends Bloc<SalesEvent, SalesState> {
  final SalesRepository _repository;
  
  SalesBloc(this._repository) : super(SalesInitial());
}
3

Register in DI

// di/injection.dart
@module
abstract class AppModule {
  @lazySingleton
  SalesBloc salesBloc(SalesRepository repo) => SalesBloc(repo);
}
4

Provide at Route Level

// cmd/routes/ops/sales_routes.dart
GoRoute(
  path: '/sales',
  builder: (context, state) => BlocProvider(
    create: (_) => getIt<SalesBloc>(),
    child: const SalesPage(),
  ),
);
5

Access in Widgets

// pages/sales_page.dart
class SalesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SalesBloc, SalesState>(
      builder: (context, state) {
        // Use state here
      },
    );
  }
}

๐Ÿ“Š Architecture Summary

PrinsipRekomendasi
Scope utama BLoCPer feature route (domain boundary)
Scope globalHanya untuk state universal (auth, theme)
Page/Component levelGunakan Cubit, bukan BLoC
Lifecycle ruleโ€Bloc lives as long as the feature route lives.โ€
Konsekuensi baiknya- Tidak recreate state
- Modular domain boundary
- Lebih mudah di test dan DI

๐Ÿ’ก Key Takeaways

BLoC Domain

Di route-level (feature provider)Contoh: /sales, /inventory

Cubit Ringan

Di page/component levelContoh: Filter, Sort, Tab Index

Root BLoC

Hanya untuk global shared stateContoh: Auth, Theme, Locale

Never in Component

Jangan taruh BLoC di widget kecilAkan recreate setiap build


Best Practice: Selalu provide BLoC di route-level untuk proper lifecycle management dan avoid memory leaks.
Avoid: Jangan provide BLoC di component-level widget. Ini akan cause recreate setiap rebuild dan memory issues.