iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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++; });

Test yourself

Q1. A StatefulWidget keeps state in…
Q2. Trigger a re-render with…
Q3. For purely declarative children with no internal state use…

Discussion

Loading…