Answer
Overview
StatefulBuilder is a widget that allows you to rebuild part of the widget tree with its own
text
setStateProblem Without StatefulBuilder
text
setStatedartclass MyPage extends StatefulWidget { _MyPageState createState() => _MyPageState(); } class _MyPageState extends State<MyPage> { bool isChecked = false; Widget build(BuildContext context) { return Scaffold( body: Column( children: [ Text('Expensive widget that rebuilds unnecessarily'), ExpensiveList(), // ❌ Rebuilds even though it shouldn't Checkbox( value: isChecked, onChanged: (value) { setState(() { isChecked = value!; }); // ❌ Entire widget tree rebuilds }, ), ], ), ); } }
Solution: StatefulBuilder
Rebuild only the StatefulBuilder widget, not the parent.
dartclass MyPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: Column( children: [ Text('This does NOT rebuild'), ExpensiveList(), // ✅ Does NOT rebuild StatefulBuilder( builder: (context, setState) { bool isChecked = false; return Checkbox( value: isChecked, onChanged: (value) { setState(() { isChecked = value!; }); // ✅ Only StatefulBuilder rebuilds }, ); }, ), ], ), ); } }
Common Use Cases
1. Dialogs with State
dartvoid showCustomDialog(BuildContext context) { bool agreedToTerms = false; showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Terms and Conditions'), content: StatefulBuilder( builder: (context, setState) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text('Please agree to the terms'), CheckboxListTile( title: Text('I agree'), value: agreedToTerms, onChanged: (value) { setState(() { agreedToTerms = value!; }); // ✅ Only dialog content rebuilds }, ), ], ); }, ), actions: [ TextButton( onPressed: agreedToTerms ? () => Navigator.pop(context) : null, child: Text('Continue'), ), ], ); }, ); }
2. Bottom Sheets
dartvoid showFilterSheet(BuildContext context) { int selectedCategory = 0; showModalBottomSheet( context: context, builder: (context) { return StatefulBuilder( builder: (context, setState) { return Column( children: [ Text('Filter by Category'), RadioListTile( title: Text('Electronics'), value: 0, groupValue: selectedCategory, onChanged: (value) { setState(() { selectedCategory = value!; }); }, ), RadioListTile( title: Text('Clothing'), value: 1, groupValue: selectedCategory, onChanged: (value) { setState(() { selectedCategory = value!; }); }, ), ElevatedButton( onPressed: () { Navigator.pop(context, selectedCategory); }, child: Text('Apply Filter'), ), ], ); }, ); }, ); }
3. Nested Widgets with Isolated State
dartclass ProductCard extends StatelessWidget { final Product product; const ProductCard({required this.product}); Widget build(BuildContext context) { return Card( child: Column( children: [ Image.network(product.imageUrl), Text(product.name), StatefulBuilder( builder: (context, setState) { int quantity = 1; return Row( children: [ IconButton( icon: Icon(Icons.remove), onPressed: () { setState(() { if (quantity > 1) quantity--; }); }, ), Text('$quantity'), IconButton( icon: Icon(Icons.add), onPressed: () { setState(() { quantity++; }); }, ), ElevatedButton( onPressed: () { addToCart(product, quantity); }, child: Text('Add to Cart'), ), ], ); }, ), ], ), ); } }
Complete Example: Multi-Step Form in Dialog
dartvoid showMultiStepDialog(BuildContext context) { int currentStep = 0; String name = ''; String email = ''; showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Registration'), content: StatefulBuilder( builder: (context, setState) { return Column( mainAxisSize: MainAxisSize.min, children: [ if (currentStep == 0) ...[ Text('Step 1: Enter Name'), TextField( onChanged: (value) => name = value, decoration: InputDecoration(labelText: 'Name'), ), ] else if (currentStep == 1) ...[ Text('Step 2: Enter Email'), TextField( onChanged: (value) => email = value, decoration: InputDecoration(labelText: 'Email'), ), ] else ...[ Text('Step 3: Confirm'), Text('Name: $name'), Text('Email: $email'), ], ], ); }, ), actions: [ if (currentStep > 0) TextButton( onPressed: () { setState(() { currentStep--; }); }, child: Text('Back'), ), TextButton( onPressed: () { if (currentStep < 2) { setState(() { currentStep++; }); } else { Navigator.pop(context); submitForm(name, email); } }, child: Text(currentStep < 2 ? 'Next' : 'Submit'), ), ], ); }, ); }
StatefulBuilder vs StatefulWidget
| Feature | StatefulBuilder | StatefulWidget |
|---|---|---|
| Use case | Isolated state changes | Full widget state |
| Boilerplate | Low (inline) | High (2 classes) |
| State lifetime | Scoped to builder | Entire widget lifecycle |
| Best for | Dialogs, bottom sheets | Pages, complex widgets |
Alternative: ValueNotifier
For simple state, use
text
ValueNotifiertext
ValueListenableBuilderdartvoid showDialogWithValueNotifier(BuildContext context) { final agreedToTerms = ValueNotifier<bool>(false); showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Terms'), content: ValueListenableBuilder<bool>( valueListenable: agreedToTerms, builder: (context, value, child) { return CheckboxListTile( title: Text('I agree'), value: value, onChanged: (newValue) { agreedToTerms.value = newValue!; }, ); }, ), ); }, ); }
Best Practices
dart// ✅ Use StatefulBuilder for isolated state StatefulBuilder( builder: (context, setState) { return Checkbox(value: isChecked, onChanged: (val) => setState(() {})); }, ) // ✅ Use for dialogs/bottom sheets with interactive widgets showDialog( builder: (context) => AlertDialog( content: StatefulBuilder(...), ), ) // ❌ Don't overuse for complex state (use BLoC, Riverpod instead) // ❌ Don't nest multiple StatefulBuilders (hard to maintain)
Summary
| Use Case | Solution |
|---|---|
| Dialog with checkbox | StatefulBuilder |
| Bottom sheet with radio buttons | StatefulBuilder |
| Nested widget with local state | StatefulBuilder |
| Full page state | StatefulWidget |
| Global state | BLoC, Riverpod, Provider |
When to use: Isolated state changes in dialogs, bottom sheets, or nested widgets without rebuilding the parent.
Learn more: StatefulBuilder Documentation