StatefulWidget
StatefulWidget holds mutable state. The widget itself is immutable; its State object survives across rebuilds. setState triggers a rebuild; initState / dispose bracket the lifecycle.
Lifecycle, setState, mounted, AnimationController
EXAMPLE
import 'package:flutter/material.dart';
import 'dart:async';
// 1) Basic stateful widget
class Counter extends StatefulWidget {
const Counter({super.key, this.initial = 0});
final int initial;
@@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
late int _count;
@@override
void initState() {
super.initState();
_count = widget.initial;
// One-time work: subscribe, fetch, controllers
}
@@override
void didUpdateWidget(covariant Counter oldWidget) {
super.didUpdateWidget(oldWidget);
// Parent passed new props — react to them
if (widget.initial != oldWidget.initial) {
setState(() => _count = widget.initial);
}
}
@@override
void dispose() {
// Symmetric to initState — controllers, streams, listeners
super.dispose();
}
void _bump() => setState(() => _count++);
@@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count', style: Theme.of(context).textTheme.headlineMedium),
FilledButton(onPressed: _bump, child: const Text('+1')),
],
);
}
}
// 2) Async work — handle mounted to avoid setState after dispose
class UserPage extends StatefulWidget {
const UserPage({super.key, required this.id});
final String id;
@@override State<UserPage> createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
User? _user;
bool _loading = true;
String? _error;
@@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final user = await api.fetchUser(widget.id);
if (!mounted) return; // widget was disposed during fetch
setState(() { _user = user; _loading = false; });
} catch (e) {
if (!mounted) return;
setState(() { _error = e.toString(); _loading = false; });
}
}
@@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
if (_error != null) return Center(child: Text('Error: $_error'));
return UserCard(user: _user!);
}
}
// 3) Lifecycle order
// createState — once per StatefulWidget creation
// initState — once, before first build
// didChangeDependencies — whenever an InheritedWidget (Theme, MediaQuery) changes
// build — every rebuild
// didUpdateWidget — when the parent passes new props
// deactivate — removed from the tree (might come back, e.g. GlobalKey reparent)
// dispose — final cleanup, won't be called again
// build runs MANY times; initState + dispose run ONCE each
// 4) AnimationController — needs TickerProviderStateMixin
class FadeIn extends StatefulWidget {
const FadeIn({super.key, required this.child, this.duration = const Duration(milliseconds: 300)});
final Widget child;
final Duration duration;
@@override State<FadeIn> createState() => _FadeInState();
}
class _FadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<double> _opacity;
@@override
void initState() {
super.initState();
_ctrl = AnimationController(vsync: this, duration: widget.duration)..forward();
_opacity = CurvedAnimation(parent: _ctrl, curve: Curves.easeOut);
}
@@override
void dispose() {
_ctrl.dispose(); // CRITICAL — leaks otherwise
super.dispose();
}
@@override
Widget build(BuildContext context) {
return FadeTransition(opacity: _opacity, child: widget.child);
}
}
// 5) Stream subscription
class NetworkStatus extends StatefulWidget {
const NetworkStatus({super.key});
@@override State<NetworkStatus> createState() => _NetworkStatusState();
}
class _NetworkStatusState extends State<NetworkStatus> {
StreamSubscription<bool>? _sub;
bool _online = true;
@@override
void initState() {
super.initState();
_sub = connectivity.statusStream.listen((online) {
if (mounted) setState(() => _online = online);
});
}
@@override
void dispose() {
_sub?.cancel();
super.dispose();
}
@@override
Widget build(BuildContext context) {
return Text(_online ? 'Online' : 'Offline');
}
}
// 6) When to use stateless vs stateful
// Stateless: pure function of props — most widgets are stateless
// Stateful : LOCAL state (animations, scroll, form inputs, controllers)
// External : Provider/Riverpod/Bloc — app-wide state, not Stateful
// 7) Performance — const + keys + extracting widgets
class Card extends StatefulWidget { … }
// If only PART of the widget needs state, extract that part:
class Header extends StatelessWidget { … } // never rebuilds
class CounterBody extends StatefulWidget { … } // only this rebuilds on setState
Return const widgets where possible — they're cached.
// 8) Common bugs
// • Calling setState outside the widget's lifetime (after dispose) → 'setState() called on disposed widget'
// • Forgetting to dispose controllers → memory + battery leaks (animations keep ticking)
// • Accessing InheritedWidget (Theme, MediaQuery) in initState → it's not safe; use didChangeDependencies
// • Long-running async work in build() — runs every rebuild
// • Mutating state without setState — UI doesn't update
// • Capturing context across async gaps — use BuildContext.mounted (Dart 3.2+)
Why it matters
The Stateful lifecycle has four loud rules: initialise in initState, react to prop changes in didUpdateWidget, check mounted before setState across async gaps, dispose everything in dispose. Skip any one and you ship leaks or crashes.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int n = 0;
@override
Widget build(BuildContext context) =>
TextButton(onPressed: () => setState(() => n++), child: Text('$n'));
}
Try it Yourself »
Exercise
Trigger a rebuild after mutating state.
(() { n++; });
camelCase.
Discussion
Loading…