State is data that can change over time. When it changes, Flutter rebuilds the widgets that depend on it.
Local state lives in a State object. setState() marks that object dirty so build() runs again.
class _CounterState extends State<Counter> {int count = 0;void increment() {setState(() {count++;});}@overrideWidget build(BuildContext context) {return ElevatedButton(onPressed: increment,child: Text("$count"),);}}
Rules:
setState, then Flutter rebuildssetState rebuilds (plus its descendants)When several widgets need the same data, lift it up or use a ChangeNotifier.
ChangeNotifier holds shared state and notifies listeners when it changes.
class CounterModel extends ChangeNotifier {int count = 0;void increment() {count++;notifyListeners();}}
Create it once, dispose it when done, and listen from the UI:
final counter = CounterModel();counter.addListener(() {print(counter.count);});counter.increment();counter.dispose();
notifyListeners() is the equivalent of setState for objects that are not widgets.
Typical use:
StateListenableBuilder so only the listening subtree rebuildsRebuilds when a Listenable (including ChangeNotifier) calls notifyListeners().
class CounterPage extends StatefulWidget {const CounterPage({super.key});@overrideState<CounterPage> createState() => _CounterPageState();}class _CounterPageState extends State<CounterPage> {final counter = CounterModel();@overridevoid dispose() {counter.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return ListenableBuilder(listenable: counter,builder: (context, child) {return Column(children: [Text("Count: ${counter.count}"),ElevatedButton(onPressed: counter.increment,child: const Text("Add"),),],);},);}}
The optional child is built once and reused — put expensive widgets there if they do not depend on the listenable.
ValueListenableBuilder is the same idea for a ValueNotifier<T>.
A StatelessWidget is Constructor → build(). A StatefulWidget has three paths into build():
createState → initState → didChangeDependencies → buildsetState → builddidUpdateWidget → buildState objects have a lifecycle. These are the methods you use most:
class _ProfileState extends State<Profile> {late final TextEditingController controller;@overridevoid initState() {super.initState();controller = TextEditingController();// subscribe, start animation, load once}@overridevoid didChangeDependencies() {super.didChangeDependencies();// Theme.of(context), InheritedWidget — after initState and when they change}@overridevoid didUpdateWidget(covariant Profile oldWidget) {super.didUpdateWidget(oldWidget);// parent passed new widget config (new props)}@overrideWidget build(BuildContext context) {return TextField(controller: controller);}@overridevoid dispose() {controller.dispose();// cancel timers, close streams, dispose notifierssuper.dispose();}}
Order (simplified):
initState → created, no context inherited widgets yetdidChangeDependencies → first time, then when inherited data changesbuild → as often as neededdidUpdateWidget → parent rebuilt this widget with a new configurationdispose → removed forever; do not call setState after thissetState can run between initState and dispose. Create controllers and listeners in initState, tear them down in dispose.