Mobile Development

State Management Flutter: Riverpod & BLoC

Tutorial lengkap state management Flutter β€” konsep dasar state, Provider, Riverpod, BLoC pattern, perbandingan solusi, dan contoh kode praktis untuk arsitektur yang bersih

1. Konsep State Management

Dalam pengembangan aplikasi Flutter, state adalah data yang dapat berubah selama lifecycle aplikasi β€” mulai dari data pengguna yang sedang login, isi keranjang belanja, hingga status loading halaman. State management yang baik adalah kunci untuk membangun aplikasi yang scalable, testable, dan mudah di-maintain.

Mengapa State Management Penting?

Tanpa State Management Dengan State Management
Data tersebar di banyak widgetData terpusat dan terorganisir
Sulit berbagi data antar widgetKomunikasi antar widget mudah dan terstruktur
Callback hell (prop drilling)Akses data langsung dari sumbernya
Sulit di-testMudah unit test dan mocking
Kode spaghettiArsitektur bersih (Clean Architecture)

Jenis-jenis State dalam Flutter

Diagram: Jenis State dalam Flutter
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    JENIS STATE                            β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Ephemeral State  β”‚  β”‚    App State (Shared)      β”‚  β”‚
β”‚  β”‚   (UI State)       β”‚  β”‚                            β”‚  β”‚
β”‚  β”‚                    β”‚  β”‚  β€’ User authentication     β”‚  β”‚
β”‚  β”‚  β€’ Checkbox state  β”‚  β”‚  β€’ Shopping cart           β”‚  β”‚
β”‚  β”‚  β€’ Current tab     β”‚  β”‚  β€’ User preferences        β”‚  β”‚
β”‚  β”‚  β€’ Expansion panel β”‚  β”‚  β€’ Theme mode              β”‚  β”‚
β”‚  β”‚  β€’ Text field valueβ”‚  β”‚  β€’ API data / cache        β”‚  β”‚
β”‚  β”‚                    β”‚  β”‚  β€’ Notification state      β”‚  β”‚
β”‚  β”‚  Solusi:           β”‚  β”‚                            β”‚  β”‚
β”‚  β”‚  StatefulWidget    β”‚  β”‚  Solusi:                   β”‚  β”‚
β”‚  β”‚  setState()        β”‚  β”‚  Provider / Riverpod / BLoCβ”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Ephemeral state adalah state lokal yang hanya relevan untuk satu widget tertentu β€” seperti status expand/collapse. Sedangkan app state adalah state yang perlu dibagikan ke banyak bagian aplikasi β€” seperti data user atau keranjang belanja.

Prinsip State Management yang Baik

πŸ’‘ Tips

Untuk aplikasi kecil atau halaman sederhana, setState() sudah cukup. Jangan over-engineer dengan BLoC atau Riverpod jika hanya mengelola satu checkbox. Gunakan state management library hanya ketika Anda perlu berbagi state antar banyak widget atau memisahkan logika bisnis.

2. setState & Pendekatan Dasar

setState() adalah metode paling dasar untuk mengelola state di Flutter. Ini bekerja dengan memperbarui variabel internal StatefulWidget dan memberitahu framework untuk membangun ulang widget. Cocok untuk state sederhana yang hanya relevan di satu halaman.

Dasar StatefulWidget

Dart β€” StatefulWidget Dasar
import 'package:flutter/material.dart';

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

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  void _decrement() {
    setState(() {
      _count--;
    });
  }

  void _reset() {
    setState(() {
      _count = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter App'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: _reset,
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              '$_count',
              style: Theme.of(context).textTheme.displayLarge,
            ),
            const SizedBox(height: 16),
            Text(
              'Tekan tombol untuk mengubah nilai',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'decrement',
            onPressed: _decrement,
            child: const Icon(Icons.remove),
          ),
          const SizedBox(width: 16),
          FloatingActionButton(
            heroTag: 'increment',
            onPressed: _increment,
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}

Masalah setState di Aplikasi Besar

Dart β€” Prop Drilling Problem
// ═══ MASALAH: Prop Drilling ═══
// Data harus dilempar dari parent ke child yang sangat dalam

class ShoppingCartScreen extends StatefulWidget {
  @override
  State<ShoppingCartScreen> createState() => _ShoppingCartScreenState();
}

class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
  List<CartItem> _cartItems = [];

  // ❌ State disimpan di screen, tapi dibutuhkan oleh banyak widget
  // ❌ Harus mengoper callback ke semua level widget tree

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Level 1: Header
        CartHeader(
          itemCount: _cartItems.length,  // dilempar ke bawah
          onClear: () { setState(() => _cartItems.clear()); },
        ),

        // Level 2: List
        Expanded(
          child: CartList(
            items: _cartItems,           // dilempar ke bawah
            onRemove: (id) {
              setState(() {
                _cartItems.removeWhere((item) => item.id == id);
              });
            },
            // ❌ Callback harus dioper sampai ke level terdalam
          ),
        ),

        // Level 3: Footer
        CartFooter(
          total: _cartItems.fold(0.0, (sum, item) => sum + item.price),
          onCheckout: () { /* ... */ },
        ),
      ],
    );
  }
}

// ❌ Masalah: Semakin dalam widget tree, semakin banyak parameter!
// ❌ Setiap widget intermediate harus menerima semua callback
// ❌ Sulit di-refactor dan test
⚠️ Kapan setState Tidak Cukup

Gunakan state management library ketika:

  • State perlu diakses dari widget yang jauh di widget tree
  • Ada banyak parameter yang dioper melalui banyak widget (prop drilling)
  • State perlu diakses dari banyak halaman
  • Anda perlu melakukan operasi async yang kompleks
  • Logika bisnis harus terpisah dari UI untuk unit testing

3. Provider

Provider adalah state management yang direkomendasikan oleh tim Flutter sendiri. Provider menggunakan konsep InheritedWidget untuk menyediakan data ke widget tree tanpa perlu mengoper parameter secara manual. Ini adalah solusi middle-ground yang baik sebelum melangkah ke Riverpod atau BLoC.

Setup Provider

YAML β€” pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.2

ChangeNotifierProvider

Dart β€” ChangeNotifierProvider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// ═══ Model ═══
class Todo {
  final String id;
  final String title;
  bool isDone;

  Todo({required this.id, required this.title, this.isDone = false});
}

// ═══ ChangeNotifier β€” State Holder ═══
class TodoProvider with ChangeNotifier {
  final List<Todo> _todos = [];

  List<Todo> get todos => List.unmodifiable(_todos);
  int get activeCount => _todos.where((t) => !t.isDone).length;
  int get doneCount => _todos.where((t) => t.isDone).length;

  void addTodo(String title) {
    _todos.add(Todo(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      title: title,
    ));
    notifyListeners();  // Memberitahu widget untuk rebuild
  }

  void toggleTodo(String id) {
    final todo = _todos.firstWhere((t) => t.id == id);
    todo.isDone = !todo.isDone;
    notifyListeners();
  }

  void removeTodo(String id) {
    _todos.removeWhere((t) => t.id == id);
    notifyListeners();
  }
}

// ═══ Main β€” Provide ke seluruh app ═══
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => TodoProvider(),
      child: const MyApp(),
    ),
  );
}

// ═══ Consumer Widget ═══
class TodoListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo List'),
        // Consumer memungkinkan akses provider
        actions: [
          Consumer<TodoProvider>(
            builder: (context, todoProvider, child) {
              return Chip(
                label: Text('${todoProvider.activeCount} aktif'),
              );
            },
          ),
        ],
      ),
      body: Consumer<TodoProvider>(
        builder: (context, todoProvider, child) {
          if (todoProvider.todos.isEmpty) {
            return const Center(
              child: Text('Belum ada todo. Tambah yang baru!'),
            );
          }

          return ListView.builder(
            itemCount: todoProvider.todos.length,
            itemBuilder: (context, index) {
              final todo = todoProvider.todos[index];
              return ListTile(
                leading: Checkbox(
                  value: todo.isDone,
                  onChanged: (_) => todoProvider.toggleTodo(todo.id),
                ),
                title: Text(
                  todo.title,
                  style: TextStyle(
                    decoration: todo.isDone
                        ? TextDecoration.lineThrough
                        : null,
                  ),
                ),
                trailing: IconButton(
                  icon: const Icon(Icons.delete),
                  onPressed: () => todoProvider.removeTodo(todo.id),
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddDialog(BuildContext context) {
    final controller = TextEditingController();
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Tambah Todo'),
        content: TextField(
          controller: controller,
          autofocus: true,
          decoration: const InputDecoration(hintText: 'Nama todo...'),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Batal'),
          ),
          ElevatedButton(
            onPressed: () {
              if (controller.text.isNotEmpty) {
                context.read<TodoProvider>().addTodo(controller.text);
                Navigator.pop(context);
              }
            },
            child: const Text('Tambah'),
          ),
        ],
      ),
    );
  }
}

Selector β€” Optimasi Rebuild

Dart β€” Selector Optimization
// Selector hanya rebuild saat data tertentu berubah
// Berguna ketika state besar tapi hanya sebagian yang ditampilkan

class CartBadge extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Hanya rebuild saat itemCount berubah, bukan saat seluruh cart berubah
    return Selector<CartProvider, int>(
      selector: (_, cart) => cart.items.length,
      builder: (context, itemCount, child) {
        return Badge(
          label: Text('$itemCount'),
          child: child,
        );
      },
      child: const Icon(Icons.shopping_cart),
    );
  }
}

// context.read β€” untuk akses sekali (di callback/onPressed)
// context.watch β€” untuk akses yang perlu rebuild saat berubah
// Consumer β€” alternatif watch untuk scope lebih kecil
πŸ’‘ Provider vs InheritedWidget

Provider adalah "wrapper" yang lebih baik dari InheritedWidget. Ia menangani disposal, lazy loading, dan menghindari boilerplate code. Gunakan context.read() untuk akses satu kali (di callback) dan context.watch() untuk akses yang butuh rebuild.

4. Riverpod

Riverpod adalah evolusi dari Provider yang dibuat oleh Remi Rousselet (pembuat Provider). Riverpod memperbaiki banyak kekurangan Provider β€” tidak memerlukan InheritedWidget, bisa digunakan di mana saja (termasuk di main()), mendukung multiple providers dari tipe yang sama, dan memiliki compile-time safety.

Setup Riverpod

YAML β€” pubspec.yaml
dependencies:
  flutter_riverpod: ^2.6.1
  riverpod_annotation: ^2.6.1

dev_dependencies:
  riverpod_generator: ^2.6.2
  build_runner: ^2.4.13

Riverpod Dasar

Dart β€” Riverpod Provider
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ═══ Provider Types ═══

// 1. Provider β€” value yang tidak berubah (computed)
final greetingProvider = Provider<String>((ref) {
  return 'Selamat datang di BeebaneLabs!';
});

// 2. StateProvider β€” value sederhana yang bisa berubah
final counterProvider = StateProvider<int>((ref) => 0);

// 3. StateNotifierProvider β€” logic complex
class TodoList extends StateNotifier<List<Todo>> {
  TodoList() : super([]);

  void add(String title) {
    state = [...state, Todo(
      id: DateTime.now().toString(),
      title: title,
    )];
  }

  void toggle(String id) {
    state = [
      for (final todo in state)
        if (todo.id == id)
          Todo(id: todo.id, title: todo.title, isDone: !todo.isDone)
        else
          todo,
    ];
  }

  void remove(String id) {
    state = state.where((t) => t.id != id).toList();
  }
}

final todoListProvider = StateNotifierProvider<TodoList, List<Todo>>(
  (ref) => TodoList(),
);

// 4. FutureProvider β€” untuk async data
final userProvider = FutureProvider<User>((ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/user'));
  return User.fromJson(jsonDecode(response.body));
});

// 5. StreamProvider β€” untuk real-time data
final messagesProvider = StreamProvider<List<Message>>((ref) {
  return FirebaseFirestore.instance
      .collection('messages')
      .orderBy('timestamp')
      .snapshots()
      .map((snapshot) => snapshot.docs
          .map((doc) => Message.fromJson(doc.data()))
          .toList());
});

ConsumerWidget & AsyncValue

Dart β€” Riverpod UI
// ═══ Main dengan ProviderScope ═══
void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

// ═══ ConsumerWidget β€” Widget yang bisa membaca provider ═══
class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Baca state
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Text(
          '$count',
          style: Theme.of(context).textTheme.displayLarge,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Update state
          ref.read(counterProvider.notifier).state++;
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

// ═══ AsyncValue β€” Handling Async State ═══
class UserProfilePage extends ConsumerWidget {
  const UserProfilePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Profil')),
      body: userAsync.when(
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.error, size: 64, color: Colors.red),
              const SizedBox(height: 16),
              Text('Error: $error'),
              ElevatedButton(
                onPressed: () => ref.invalidate(userProvider),
                child: const Text('Coba Lagi'),
              ),
            ],
          ),
        ),
        data: (user) => ListView(
          padding: const EdgeInsets.all(16),
          children: [
            CircleAvatar(
              radius: 50,
              child: Text(user.name[0], style: const TextStyle(fontSize: 32)),
            ),
            const SizedBox(height: 16),
            Text(user.name, style: Theme.of(context).textTheme.headlineMedium),
            Text(user.email),
          ],
        ),
      ),
    );
  }
}

// ═══ Computed/Dependent Provider ═══
final activeTodosProvider = Provider<List<Todo>>((ref) {
  final todos = ref.watch(todoListProvider);
  return todos.where((t) => !t.isDone).toList();
});

final completedCountProvider = Provider<int>((ref) {
  final todos = ref.watch(todoListProvider);
  return todos.where((t) => t.isDone).length;
});
Diagram: Riverpod Data Flow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 RIVERPOD ARCHITECTURE                    β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  UI /    │────▢│  Provider   │────▢│  Repository  β”‚  β”‚
β”‚  β”‚  Screen  │◀────│  (State)    │◀────│  / API       β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚       β”‚                β”‚                     β”‚           β”‚
β”‚       β”‚           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚           β”‚
β”‚       β”‚           β”‚  Auto       β”‚            β”‚           β”‚
β”‚       └──────────▢│  Dispose    β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                   β”‚  & Cache    β”‚                        β”‚
β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                        β”‚
β”‚                                                          β”‚
β”‚  ConsumerWidget ─── watch/read ───▢ Provider            β”‚
β”‚  AsyncValue ─── when(data/error/loading) ───▢ UI       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’‘ Riverpod Code Generation

Dengan riverpod_generator, Anda bisa menggunakan anotasi @riverpod untuk menghasilkan provider secara otomatis. Ini mengurangi boilerplate dan menghindari kesalahan typing pada key provider.

5. BLoC Pattern

BLoC (Business Logic Component) adalah pattern yang memisahkan business logic dari UI menggunakan Streams dan Events. BLoC sangat cocok untuk proyek besar dengan banyak developer karena enforce struktur yang konsisten.

Setup BLoC

YAML β€” pubspec.yaml
dependencies:
  flutter_bloc: ^8.1.6
  equatable: ^2.0.5
  bloc: ^8.1.4

BLoC Structure

Dart β€” BLoC Complete Example
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

// ═══ 1. EVENTS β€” Input yang dikirim ke BLoC ═══
abstract class AuthEvent extends Equatable {
  @override
  List<Object?> get props => [];
}

class LoginRequested extends AuthEvent {
  final String email;
  final String password;

  LoginRequested({required this.email, required this.password});

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

class LogoutRequested extends AuthEvent {}

class CheckAuthStatus extends AuthEvent {}

// ═══ 2. STATES β€” Output yang di-emit oleh BLoC ═══
abstract class AuthState extends Equatable {
  @override
  List<Object?> get props => [];
}

class AuthInitial extends AuthState {}

class AuthLoading extends AuthState {}

class Authenticated extends AuthState {
  final User user;
  Authenticated(this.user);

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

class AuthError extends AuthState {
  final String message;
  AuthError(this.message);

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

// ═══ 3. BLOCS β€” Logic yang memproses events menjadi states ═══
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository authRepository;

  AuthBloc({required this.authRepository}) : super(AuthInitial()) {
    // Register event handlers
    on<LoginRequested>(_onLoginRequested);
    on<LogoutRequested>(_onLogoutRequested);
    on<CheckAuthStatus>(_onCheckAuthStatus);
  }

  Future<void> _onLoginRequested(
    LoginRequested event,
    Emitter<AuthState> emit,
  ) async {
    emit(AuthLoading());
    try {
      final user = await authRepository.login(
        email: event.email,
        password: event.password,
      );
      emit(Authenticated(user));
    } catch (e) {
      emit(AuthError(e.toString()));
    }
  }

  Future<void> _onLogoutRequested(
    LogoutRequested event,
    Emitter<AuthState> emit,
  ) async {
    await authRepository.logout();
    emit(AuthInitial());
  }

  Future<void> _onCheckAuthStatus(
    CheckAuthStatus event,
    Emitter<AuthState> emit,
  ) async {
    final user = await authRepository.getCurrentUser();
    if (user != null) {
      emit(Authenticated(user));
    } else {
      emit(AuthInitial());
    }
  }
}

BLoC UI Integration

Dart β€” BLoC di UI
// ═══ BlocProvider di Main ═══
void main() {
  runApp(
    BlocProvider(
      create: (context) => AuthBloc(
        authRepository: AuthRepository(),
      )..add(CheckAuthStatus()),  // Check auth saat app mulai
      child: const MyApp(),
    ),
  );
}

// ═══ Login Screen dengan BLoC ═══
class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: BlocListener<AuthBloc, AuthState>(
        listener: (context, state) {
          if (state is Authenticated) {
            // Navigate ke home
            Navigator.pushReplacementNamed(context, '/home');
          } else if (state is AuthError) {
            // Tampilkan snackbar error
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(state.message),
                backgroundColor: Colors.red,
              ),
            );
          }
        },
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextField(
                controller: _emailController,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  border: OutlineInputBorder(),
                ),
                keyboardType: TextInputType.emailAddress,
              ),
              const SizedBox(height: 16),
              TextField(
                controller: _passwordController,
                decoration: const InputDecoration(
                  labelText: 'Password',
                  border: OutlineInputBorder(),
                ),
                obscureText: true,
              ),
              const SizedBox(height: 24),

              // BlocBuilder untuk rebuild berdasarkan state
              BlocBuilder<AuthBloc, AuthState>(
                builder: (context, state) {
                  final isLoading = state is AuthLoading;

                  return SizedBox(
                    width: double.infinity,
                    height: 56,
                    child: ElevatedButton(
                      onPressed: isLoading ? null : () {
                        context.read<AuthBloc>().add(
                          LoginRequested(
                            email: _emailController.text,
                            password: _passwordController.text,
                          ),
                        );
                      },
                      child: isLoading
                          ? const CircularProgressIndicator()
                          : const Text('Login'),
                    ),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ═══ BlocConsumer β€” Combine Listener + Builder ═══
class DashboardPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocConsumer<AuthBloc, AuthState>(
      // Listener untuk side effects (navigate, snackbar)
      listener: (context, state) {
        if (state is AuthInitial) {
          Navigator.pushReplacementNamed(context, '/login');
        }
      },
      // Builder untuk rebuild UI
      builder: (context, state) {
        if (state is Authenticated) {
          return Scaffold(
            appBar: AppBar(
              title: Text('Dashboard - ${state.user.name}'),
              actions: [
                IconButton(
                  icon: const Icon(Icons.logout),
                  onPressed: () {
                    context.read<AuthBloc>().add(LogoutRequested());
                  },
                ),
              ],
            ),
            body: Center(
              child: Text('Selamat datang, ${state.user.name}!'),
            ),
          );
        }
        return const Center(child: CircularProgressIndicator());
      },
    );
  }
}
Diagram: BLoC Data Flow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   BLoC PATTERN FLOW                      β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚   UI /   │───▢│  Events  │───▢│   BLoC   β”‚           β”‚
β”‚  β”‚  Screen  β”‚    β”‚  (Input) β”‚    β”‚ (Logic)  β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚       β–²                                  β”‚               β”‚
β”‚       β”‚                                  β–Ό               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚  β”‚  States  │◀────────────────────│  Reposi- β”‚          β”‚
β”‚  β”‚ (Output) β”‚                     β”‚  tory    β”‚          β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
β”‚                                                          β”‚
β”‚  Event ───▢ BLoC.process() ───▢ emit(State) ───▢ UI    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

6. Perbandingan Semua Solusi

Aspek setState Provider Riverpod BLoC
Complexity🟒 Rendah🟑 Sedang🟑 SedangπŸ”΄ Tinggi
Learning Curve🟒 Mudah🟒 Mudah🟑 SedangπŸ”΄ Curam
TestabilityπŸ”΄ Sulit🟑 Cukup🟒 Mudah🟒 Sangat Mudah
Boilerplate🟒 Minim🟑 Sedang🟒 MinimπŸ”΄ Banyak
Compile Safety🟒 YaπŸ”΄ Runtime🟒 Compile-time🟑 Cukup
Async Support🟑 Manual🟑 AsyncValue🟒 Built-in🟒 Stream
Dependency InjectionβŒβœ… Sederhanaβœ… Lengkapβœ… Modular
Cocok untukWidget lokalAplikasi kecil-sedangSemua ukuranEnterprise / Tim besar

Kapan Menggunakan Apa?

πŸ“Œ Rekomendasi Penggunaan
  • setState β€” Widget tunggal seperti form input, toggle, atau tab indicator
  • Provider β€” Aplikasi sederhana-menengah, prototyping, atau migrasi dari setState
  • Riverpod β€” Aplikasi modern, testability tinggi, atau proyek baru dari nol
  • BLoC β€” Enterprise app, tim besar, atau proyek yang butuh arsitektur sangat terstruktur
πŸ’‘ Best Practice

Tidak ada "satu solusi yang benar" β€” yang terbaik adalah yang sesuai dengan kebutuhan dan tim Anda. Untuk proyek baru di 2026, Riverpod adalah pilihan populer karena menggabungkan kemudahan Provider dengan kekuatan compile-time safety dan testability. Jika tim Anda sudah familiar dengan reactive programming, BLoC memberikan struktur yang sangat jelas.

7. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang state management Flutter:

Pertanyaan 1: Apa yang dimaksud dengan "state hoisting" dalam konteks Flutter state management?

a) Menyimpan semua state di dalam StatefulWidget
b) Memindahkan state ke widget parent agar child widget menjadi stateless dan reusable
c) Menyimpan state di dalam database lokal
d) Menghapus state yang tidak digunakan

Pertanyaan 2: Apa perbedaan utama antara Provider dan Riverpod?

a) Provider lebih cepat dari Riverpod
b) Riverpod tidak memerlukan InheritedWidget dan mendukung compile-time safety
c) Provider lebih baru dari Riverpod
d) Riverpod hanya bisa digunakan di web

Pertanyaan 3: Dalam BLoC pattern, apa yang dimaksud dengan "Event"?

a) Output yang ditampilkan di UI
b) Input yang dikirim ke BLoC untuk diproses menjadi state baru
c) Fungsi untuk menginisialisasi BLoC
d) Data yang disimpan di SharedPreferences

Pertanyaan 4: Apa fungsi dari notifyListeners() dalam ChangeNotifier?

a) Menghapus data dari provider
b) Memberitahu semua widget Consumer untuk rebuild dengan state terbaru
c) Menyimpan data ke local storage
d) Menginisialisasi provider baru

Pertanyaan 5: Kapan sebaiknya menggunakan setState() saja tanpa state management library?

a) Untuk semua kasus state management
b) Saat state hanya relevan untuk satu widget lokal dan tidak perlu dibagikan
c) Hanya saat menggunakan Widget yang sangat besar
d) Tidak pernah, selalu gunakan library
πŸ” Zoom
100%
🎨 Tema