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 widget | Data terpusat dan terorganisir |
| Sulit berbagi data antar widget | Komunikasi antar widget mudah dan terstruktur |
| Callback hell (prop drilling) | Akses data langsung dari sumbernya |
| Sulit di-test | Mudah unit test dan mocking |
| Kode spaghetti | Arsitektur bersih (Clean Architecture) |
Jenis-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
- Single Source of Truth β setiap data hanya disimpan di satu tempat
- Unidirectional Data Flow β data mengalir dari sumber ke UI, event dari UI ke sumber
- Separation of Concerns β UI terpisah dari logika bisnis
- Immutability β state tidak diubah secara langsung, tetapi di-replace dengan state baru
- Testability β logika bisnis bisa di-test tanpa UI
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
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
// βββ 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
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
dependencies:
flutter:
sdk: flutter
provider: ^6.1.2
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
// 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 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
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
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
// βββ 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;
});
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β RIVERPOD ARCHITECTURE β β β β ββββββββββββ βββββββββββββββ ββββββββββββββββ β β β UI / ββββββΆβ Provider ββββββΆβ Repository β β β β Screen βββββββ (State) βββββββ / API β β β ββββββββββββ βββββββββββββββ ββββββββββββββββ β β β β β β β β βββββββββββββββ β β β β β Auto β β β β ββββββββββββΆβ Dispose ββββββββββββββ β β β & Cache β β β βββββββββββββββ β β β β ConsumerWidget βββ watch/read ββββΆ Provider β β AsyncValue βββ when(data/error/loading) ββββΆ UI β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
dependencies: flutter_bloc: ^8.1.6 equatable: ^2.0.5 bloc: ^8.1.4
BLoC Structure
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
// βββ 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());
},
);
}
}
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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 untuk | Widget lokal | Aplikasi kecil-sedang | Semua ukuran | Enterprise / Tim besar |
Kapan Menggunakan Apa?
- 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
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: