setState
setState is Flutter’s built-in way for a StatefulWidget to tell the framework “rebuild my subtree.” It’s the foundation every other state-management approach builds on; mastering when (and when not) to use it sharpens your sense for re-renders and rebuilds.
StatefulWidget, lifecycle, scope, alternatives
EXAMPLE
import 'package:flutter/material.dart';
// 1) StatefulWidget + State
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++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(child: Text('Count: $_count')),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
}
// setState's callback is where you MUTATE; build() reads the new values.
// Flutter then re-runs build() and re-renders the subtree.
// 2) When NOT to call setState
// • Outside the widget tree (after dispose) → throws 'setState called after dispose'
// • From build() itself → infinite rebuild loop
// • From initState only to set initial values that don't depend on inherited widgets
// (just set the field directly)
// • For async results — guard with 'if (mounted)'
Future<void> _load() async {
final data = await api.fetchData();
if (!mounted) return; // widget may have been disposed
setState(() => _data = data);
}
// 3) State lifecycle
class _MyWidgetState extends State<MyWidget> {
@override
void initState() { super.initState(); /* one-time setup */ }
@override
void didChangeDependencies() { super.didChangeDependencies(); /* inheritable changed */ }
@override
void didUpdateWidget(MyWidget old) { super.didUpdateWidget(old); /* props changed */ }
@override
void dispose() { /* cancel subscriptions, controllers */ super.dispose(); }
@override
Widget build(BuildContext context) { /* called many times */ }
}
// 4) Scope — setState rebuilds THIS state object's subtree
// • Smaller stateful widgets = smaller rebuilds
// • Hoist state UP, not DOWN; or push state DOWN into a child if siblings don't need it
// • Use Builder() to scope rebuild to a sub-tree without making a whole new widget
class _AppState extends State<App> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
const ExpensiveHeader(), // never rebuilds; counter doesn't live here
Text('Count: $_count'),
ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+1')),
],
);
}
}
// 5) Common controllers
class _MyFormState extends State<MyForm> {
final _emailCtrl = TextEditingController();
final _scrollCtrl = ScrollController();
@override
void dispose() {
_emailCtrl.dispose();
_scrollCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(controller: _emailCtrl),
ElevatedButton(
onPressed: () => print(_emailCtrl.text),
child: const Text('Print'),
),
],
);
}
}
// Controllers are mutable; you don't need setState to read their text/values.
// You CAN trigger setState in onChanged if other UI depends on the value.
// 6) Lifting state up
class _ParentState extends State<Parent> {
int _value = 0;
void _set(int v) => setState(() => _value = v);
@override
Widget build(BuildContext context) => Column(
children: [
CounterDisplay(value: _value),
CounterInput(value: _value, onChanged: _set),
],
);
}
// 7) Async + futures + streams
class _Future extends State<F> {
late Future<Data> _future;
@override
void initState() { super.initState(); _future = api.load(); }
@override
Widget build(BuildContext context) {
return FutureBuilder<Data>(
future: _future,
builder: (ctx, snap) {
if (snap.connectionState != ConnectionState.done) return const CircularProgressIndicator();
if (snap.hasError) return Text('Error: ${snap.error}');
return DataView(data: snap.data!);
},
);
}
}
// 8) When to graduate from setState
// • Many widgets need the same state → InheritedWidget, Provider, Riverpod, Bloc, Redux
// • Complex async logic → BLoC pattern, Riverpod's async providers
// • Persistence across navigation → state in a higher-up store
// • Multi-screen forms / wizards → controllers + a model class
//
// For small apps, setState is plenty. Adding a state library too early is over-engineering.
// 9) Riverpod alternative (recommended for new Flutter apps)
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateProvider<int>((ref) => 0);
class CounterPageRp extends ConsumerWidget {
const CounterPageRp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('Count: $count')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
// 10) Avoiding excessive rebuilds — const + key
// • Mark immutable widgets as 'const' — they skip rebuild entirely
// • Use ValueListenableBuilder, AnimatedBuilder for fine-grained reactivity without full rebuild
// • Profile with the 'Performance' overlay + DevTools timeline to find rebuild hot spots
// 11) Common bugs
// • setState() called after dispose() → 'setState() called after dispose'; guard with 'if (mounted)'
// • Mutating state OUTSIDE the setState callback → UI may not update on the next frame
// • Heavy work inside the setState callback → freezes UI; do async work first, then setState with results
// • Calling setState during build → 'setState() or markNeedsBuild() called during build'
// • Forgetting to dispose controllers → memory leaks; always pair init + dispose
// • Keys missing on list rows that reorder → state attaches to wrong widget on rebuild
// • Treating widget.props as mutable — they're not; use State for mutable values
// • Premature state lib — start with setState, graduate when complexity demands it
Why it matters
setState is the foundation: mutate inside the callback, let Flutter rebuild. Keep stateful widgets small so rebuilds stay scoped, dispose controllers, guard async results with if (mounted), and graduate to Riverpod / BLoC only when multiple widgets need to share or persist the state.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…