6 manieren om de staat te beheren in Flutter-apps (met codevoorbeelden)
Hoe state te beheren in Flutter
In dit artikel bespreken we zes populaire manieren om de staat te beheren in Flutter apps, waaronder reële voorbeelden en beste praktijken:
State management is een van de belangrijkste - en meest besproken - onderwerpen in Flutter-ontwikkeling. Het bepaalt hoe je app veranderingen in gegevens afhandelt en de UI efficiënt bijwerkt. Flutter biedt je verschillende strategieën voor het beheren van staat — van eenvoudig tot zeer schaalbaar.
De strategieën voor state management in Flutter zijn:
- 🧱
setState()
— De ingebouwde, eenvoudigste aanpak - 🏗️ InheritedWidget — De basis van Flutter voor het doorgeven van staat
- 🪄 Provider — De aanbevolen oplossing voor de meeste apps
- 🔒 Riverpod — Moderne, compile-veilige evolutie van Provider
- 📦 Bloc — Voor schaalbare, enterprise-klasse toepassingen
- ⚡ GetX — Lichte all-in-one oplossing
1. Gebruik van setState()
— De basis
De eenvoudigste manier om staat te beheren in Flutter is door gebruik te maken van de ingebouwde setState()
methode in een StatefulWidget
.
Deze aanpak is ideaal voor lokale UI-stand — waarbij de staat behoort tot één widget en niet gedeeld hoeft te worden over de app.
Voorbeeld: Teller-app met setState()
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('setState Teller')),
body: Center(
child: Text('Aantal: $_count', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Icon(Icons.add),
),
);
}
}
Voordelen
- Zeer eenvoudig om te implementeren
- Ideaal voor lokale of tijdelijke UI-stand
- Geen externe afhankelijkheden
Nadelen
- Schaalbaarheid is beperkt voor grote apps
- Moeilijk om staat te delen tussen widgets
- Logica is gemengd met UI
Gebruik het wanneer:
- Prototyperen of kleine widgets bouwen
- Isolatie van UI-stand (bijvoorbeeld knoppen omschakelen, modale vensters tonen)
2. InheritedWidget — De basis van Flutter
InheritedWidget
is het laag niveau mechanisme dat Flutter gebruikt om gegevens door de widget boom te propageren. De meeste oplossingen voor state management (inclusief Provider) zijn op deze gebouwd.
Het begrijpen van InheritedWidget helpt je om te begrijpen hoe state management in Flutter werkt onder de motorkap.
Voorbeeld: Thema manager met InheritedWidget
import 'package:flutter/material.dart';
// De InheritedWidget die thema gegevens bevat
class AppTheme extends InheritedWidget {
final bool isDarkMode;
final Function toggleTheme;
const AppTheme({
Key? key,
required this.isDarkMode,
required this.toggleTheme,
required Widget child,
}) : super(key: key, child: child);
// Methode om het dichtstbijzijnde AppTheme te verkrijgen in de widget boom
static AppTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppTheme>();
}
@override
bool updateShouldNotify(AppTheme oldWidget) {
return isDarkMode != oldWidget.isDarkMode;
}
}
// Stateful wrapper om staatveranderingen te beheren
class ThemeManager extends StatefulWidget {
final Widget child;
const ThemeManager({Key? key, required this.child}) : super(key: key);
@override
_ThemeManagerState createState() => _ThemeManagerState();
}
class _ThemeManagerState extends State<ThemeManager> {
bool _isDarkMode = false;
void _toggleTheme() {
setState(() {
_isDarkMode = !_isDarkMode;
});
}
@override
Widget build(BuildContext context) {
return AppTheme(
isDarkMode: _isDarkMode,
toggleTheme: _toggleTheme,
child: widget.child,
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ThemeManager(
child: Builder(
builder: (context) {
final theme = AppTheme.of(context);
return MaterialApp(
theme: theme!.isDarkMode ? ThemeData.dark() : ThemeData.light(),
home: HomeScreen(),
);
},
),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
return Scaffold(
appBar: AppBar(title: Text('InheritedWidget Thema')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Huidig thema: ${theme!.isDarkMode ? "Donker" : "Licht"}',
style: TextStyle(fontSize: 20),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => theme.toggleTheme(),
child: Text('Thema omschakelen'),
),
],
),
),
);
}
}
Voordelen
- Ingebouwd in Flutter — geen externe afhankelijkheden
- Efficiënte widget heropbouwen
- Basis voor het begrijpen van andere oplossingen
- Directe controle over propagatie logica
Nadelen
- Veel boilerplate code
- Vereist een wrapper StatefulWidget
- Makkelijk fouten maken
- Niet beginner-vriendelijk
Gebruik het wanneer:
- Je leert hoe state management in Flutter intern werkt
- Je bouwt aangepaste state management oplossingen
- Je hebt zeer specifieke controle over staat propagatie
3. Provider — De aanbevolen oplossing van Flutter
Wanneer de staat van je app gedeeld moet worden tussen meerdere widgets, komt Provider je te hulp.
Provider is gebaseerd op Inversion of Control — in plaats van dat widgets de staat bezitten, maakt een provider deze beschikbaar voor anderen. Het Flutter team aanbeveelt het voor gemiddelde grootte apps.
Instelling
Voeg dit toe aan je pubspec.yaml
:
dependencies:
provider: ^6.0.5
Voorbeeld: Teller-app met Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Scaffold(
appBar: AppBar(title: Text('Provider Teller')),
body: Center(
child: Text('Aantal: ${counter.count}', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: context.read<CounterModel>().increment,
child: Icon(Icons.add),
),
);
}
}
Voordelen
- Reactief en efficiënt bijwerken
- Duidelijke scheiding tussen UI en logica
- Goed gedocumenteerd en community-ondersteund
Nadelen
- Slighter meer boilerplate dan
setState()
- Gekoppelde providers kunnen complex worden
Gebruik het wanneer:
- Je gedeelde staat nodig hebt over meerdere widgets
- Je een reactieve patroon wilt zonder complexiteit
- Je app groeit verder dan prototype grootte
4. Riverpod — De moderne evolutie van Provider
Riverpod is een volledige herschrijving van Provider die afhankelijkheid van BuildContext verwijdert en compile-tijd veiligheid toevoegt. Het is ontworpen om de beperkingen van Provider op te lossen terwijl het dezelfde filosofie behoudt.
Instelling
Voeg dit toe aan je pubspec.yaml
:
dependencies:
flutter_riverpod: ^2.4.0
Voorbeeld: Gebruiker profiel met Riverpod
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Model
class UserProfile {
final String name;
final int age;
UserProfile({required this.name, required this.age});
UserProfile copyWith({String? name, int? age}) {
return UserProfile(
name: name ?? this.name,
age: age ?? this.age,
);
}
}
// State Notifier
class ProfileNotifier extends StateNotifier<UserProfile> {
ProfileNotifier() : super(UserProfile(name: 'Gast', age: 0));
void updateName(String name) {
state = state.copyWith(name: name);
}
void updateAge(int age) {
state = state.copyWith(age: age);
}
}
// Provider
final profileProvider = StateNotifierProvider<ProfileNotifier, UserProfile>((ref) {
return ProfileNotifier();
});
// Computed provider
final greetingProvider = Provider<String>((ref) {
final profile = ref.watch(profileProvider);
return 'Hallo, ${profile.name}! Je bent ${profile.age} jaar oud.';
});
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: ProfileScreen());
}
}
class ProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final profile = ref.watch(profileProvider);
final greeting = ref.watch(greetingProvider);
return Scaffold(
appBar: AppBar(title: Text('Riverpod Profiel')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(greeting, style: TextStyle(fontSize: 20)),
SizedBox(height: 30),
TextField(
decoration: InputDecoration(labelText: 'Naam'),
onChanged: (value) {
ref.read(profileProvider.notifier).updateName(value);
},
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Leeftijd: ${profile.age}', style: TextStyle(fontSize: 18)),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
ref.read(profileProvider.notifier).updateAge(profile.age + 1);
},
child: Text('+'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
if (profile.age > 0) {
ref.read(profileProvider.notifier).updateAge(profile.age - 1);
}
},
child: Text('-'),
),
],
),
],
),
),
);
}
}
Voordelen
- Geen afhankelijkheid van BuildContext — staat kan worden bereikt vanaf elk punt
- Compile-tijd veiligheid — fouten kunnen worden opgevangen voor runtime
- Betere testbaarheid
- Krachtige staat samenstelling
- Geen geheugenlekken
Nadelen
- Verschillend API van traditionele Flutter patronen
- Steilere leercurve dan Provider
- Kleiner community (maar snel groeiend)
Gebruik het wanneer:
- Je een nieuw Flutter project start
- Je type-veiligheid en compile-tijd garanties wilt
- Complexe staat afhankelijkheden en samenstellingen
- Je werkt aan medium- tot grote schaal apps
5. BLoC (Business Logic Component) — Voor enterprise apps
Het BLoC patroon is een geavanceerdere architectuur die de business logica volledig van de UI schept met behulp van streams.
Het is ideaal voor grote schaal of enterprise toepassingen, waar voorspelbare en testbare staatstransities essentieel zijn.
Instelling
Voeg deze afhankelijkheid toe aan je project:
dependencies:
flutter_bloc: ^9.0.0
Voorbeeld: Teller-app met BLoC
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: MaterialApp(home: CounterScreen()),
);
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Teller')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Text('Aantal: $count', style: TextStyle(fontSize: 24));
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: Icon(Icons.add),
),
);
}
}
Voordelen
- Schaalbaar en onderhoudbaar voor complexe apps
- Duidelijke scheiding van lagen
- Eenvoudig om te testen
Nadelen
- Meer boilerplate
- Steilere leercurve
Gebruik het wanneer:
- Je enterprise of lange termijn projecten bouwt
- Je voorspelbare en testbare logica nodig hebt
- Meerdere ontwikkelaars werken aan verschillende app modules
6. GetX — De lichte all-in-one oplossing
GetX is een minimalistische maar krachtige oplossing voor state management die ook routing, afhankelijkheidsinjektie en hulpmiddelen bevat. Het is bekend om de minste boilerplate van alle oplossingen.
Instelling
Voeg dit toe aan je pubspec.yaml
:
dependencies:
get: ^4.6.5
Voorbeeld: Winkelwagentje met GetX
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// Controller
class ShoppingController extends GetxController {
// Observable list
var items = <String>[].obs;
// Observable counter
var totalItems = 0.obs;
void addItem(String item) {
if (item.isNotEmpty) {
items.add(item);
totalItems.value = items.length;
}
}
void removeItem(int index) {
items.removeAt(index);
totalItems.value = items.length;
}
void clearAll() {
items.clear();
totalItems.value = 0;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX Winkelwagentje',
home: ShoppingScreen(),
);
}
}
class ShoppingScreen extends StatelessWidget {
// Initialize controller
final ShoppingController controller = Get.put(ShoppingController());
final TextEditingController textController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Winkelwagentje'),
actions: [
Obx(() => Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
'Items: ${controller.totalItems}',
style: TextStyle(fontSize: 18),
),
),
)),
],
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: textController,
decoration: InputDecoration(
hintText: 'Voer itemnaam in',
border: OutlineInputBorder(),
),
),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
controller.addItem(textController.text);
textController.clear();
},
child: Icon(Icons.add),
),
],
),
),
Expanded(
child: Obx(() {
if (controller.items.isEmpty) {
return Center(
child: Text('Geen items in je lijst'),
);
}
return ListView.builder(
itemCount: controller.items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(controller.items[index]),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => controller.removeItem(index),
),
);
},
);
}),
),
if (controller.items.isNotEmpty)
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
Get.defaultDialog(
title: 'Lijst leegmaken',
middleText: 'Weet je zeker dat je alle items wilt verwijderen?',
textConfirm: 'Ja',
textCancel: 'Nee',
onConfirm: () {
controller.clearAll();
Get.back();
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
minimumSize: Size(double.infinity, 50),
),
child: Text('Alles leegmaken'),
),
),
],
),
);
}
}
Voordelen
- Minimale boilerplate — snelste ontwikkeling
- All-in-one oplossing (state, routing, DI, snackbars, dialogen)
- Geen BuildContext nodig
- Zeer licht en snel
- Eenvoudig om te leren
Nadelen
- Minder “Flutter-achtig” dan Provider/BLoC
- Kan leiden tot strakke koppeling als je niet voorzichtig bent
- Kleiner ecosysteem dan Provider
- Sommige ontwikkelaars vinden het “te magisch”
Gebruik het wanneer:
- Snel prototyperen of MVP-ontwikkeling
- Kleine tot gemiddelde apps
- Je minimale boilerplate wilt
- Team prefereert eenvoud over strikte architectuur
Kiezen voor de juiste state management oplossing
Oplossing | Complexiteit | Boilerplate | Leercurve | Beste voor |
---|---|---|---|---|
🧱 setState() |
Laag | Minimaal | Eenvoudig | Eenvoudige lokale staat, prototypes |
🏗️ InheritedWidget |
Gemiddeld | Hoog | Gemiddeld | Leren Flutter intern, aangepaste oplossingen |
🪄 Provider |
Laag-middel | Laag | Eenvoudig | Meeste apps, gedeelde staat |
🔒 Riverpod |
Middel | Laag-middel | Gemiddeld | Moderne apps, type veiligheid |
📦 BLoC |
Hoog | Hoog | Steil | Enterprise apps, complexe business logica |
⚡ GetX |
Laag | Minimaal | Eenvoudig | Snel ontwikkeling, MVPs |
Snelle beslissingsgids:
App complexiteit | Aanbevolen aanpak | Voorbeeld gebruik |
---|---|---|
🪶 Eenvoudig | setState() of GetX |
Knoppen omschakelen, animaties, kleine widgets |
⚖️ Gemiddeld | Provider of Riverpod |
Gedeelde thema’s, gebruikersinstellingen, gegevenscaching |
🏗️ Complex | BLoC of Riverpod |
E-commerce, chat apps, financiële dashboards |
Eindgedachten
Er is geen enkele oplossing die voor alle situaties geschikt is voor state management in Flutter.
Hier is een praktische aanpak:
- Begin klein met
setState()
voor eenvoudige widgets en prototypes - Leer de fundamenten met InheritedWidget om te begrijpen hoe Flutter intern werkt
- Groei naar Provider als je app groeit en gedeelde staat nodig heeft
- Overweeg Riverpod voor nieuwe projecten waar type veiligheid en moderne patronen belangrijk zijn
- Aanvaard BLoC voor grote enterprise toepassingen met complexe business logica
- Probeer GetX wanneer snel ontwikkeling en minimale boilerplate prioriteiten zijn
Belangrijke conclusies:
✅ Over-engineeren niet — gebruik setState()
totdat je meer nodig hebt
✅ Provider en Riverpod zijn uitstekende keuzes voor de meeste toepassingen
✅ BLoC is ideaal voor grote teams en complexe apps
✅ GetX is goed voor snelheid, maar wees bewust van koppeling
✅ Het begrijpen van InheritedWidget helpt je om elke oplossing te beheersen
Het belangrijkste is om eenvoud, schaalbaarheid en onderhoudbaarheid te balanceren — en de juiste tool te kiezen op basis van je specifieke behoeften, team expertise en project vereisten.
Flutter State Management Libraries Links
- Flutter Officiële Documentatie over State Management
- Provider Package
- Riverpod Documentatie
- BLoC Bibliothek
- GetX Documentatie