Home
Dart
OOP
Flutter
Contact

Stateless vs Stateful Widgets

Master widget states, lifecycle methods, and when to use each type.

StatelessWidget

A StatelessWidget is immutable — once created, it cannot change its internal state. Use it for static content that depends only on its constructor arguments:

greeting_card.dart
class GreetingCard extends StatelessWidget {
  final String userName;
  final String message;

  const GreetingCard({
    super.key,
    required this.userName,
    this.message = 'Welcome back!',
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Text('Hello, $userName!',
              style: Theme.of(context).textTheme.headlineSmall),
            Text(message),
          ],
        ),
      ),
    );
  }
}

StatefulWidget

A StatefulWidget is mutable. It maintains a State object that persists across rebuilds:

toggle_switch.dart
class ToggleSwitch extends StatefulWidget {
  final String label;
  final bool initialValue;
  final ValueChanged<bool>? onChanged;

  const ToggleSwitch({
    super.key,
    required this.label,
    this.initialValue = false,
    this.onChanged,
  });

  @override
  State<ToggleSwitch> createState() => _ToggleSwitchState();
}

class _ToggleSwitchState extends State<ToggleSwitch> {
  late bool _isOn;

  @override
  void initState() {
    super.initState();
    _isOn = widget.initialValue;
  }

  void _toggle() {
    setState(() {
      _isOn = !_isOn;
    });
    widget.onChanged?.call(_isOn);
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text(widget.label),
        Switch(value: _isOn, onChanged: (_) => _toggle()),
      ],
    );
  }
}

Complete Lifecycle Diagram

Understanding the widget lifecycle is critical for writing efficient Flutter apps:

MethodWhen CalledCommon Use
createState()First time widget appearsCreate the State object
initState()Once, when State is createdSubscriptions, controllers, API calls
didChangeDependencies()After initState, or when InheritedWidget changesRead Theme, MediaQuery, Locale
build()After initState, after setStateReturn widget tree
didUpdateWidget()Parent rebuilds with new configCompare old vs new props
dispose()Before removal from treeCancel subscriptions, close controllers
⚠️ Common Mistake: initState() and Build Context Never use BuildContext in initState() to look up inherited widgets (like Theme.of(context)). The context isn't fully initialized yet. Use didChangeDependencies() instead — it's called right after initState() and the context is ready.

The Build Method Contract

The build() method has strict rules that every Flutter developer must follow:

  1. Must be pure: It should only depend on the widget's state and incoming parameters.
  2. Must be fast: It's called frequently (every setState, parent rebuild, etc.).
  3. Must return a widget: Even empty containers return a widget.
  4. Must not have side effects: No network calls, no timers, no mutations.

Stateful Widget Lifecycle

MethodWhen CalledUse For
createState()First time widget inserted in treeCreate State object
initState()State created (once)Initialize subscriptions, controllers
didChangeDependencies()InheritedWidget changesRead Theme, MediaQuery, etc.
build()After initState, setState, deps changeReturn widget tree
didUpdateWidget()Parent rebuilds with new configReact to parent changes
dispose()Removed from tree permanentlyCleanup: close streams, dispose controllers

setState()

setState() notifies Flutter that internal state changed, triggering a rebuild:

counter.dart
class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  void _increment() => setState(() => _count++);

  void _decrement() => setState(() {
    if (_count > 0) _count--;
  });

  void _reset() => setState(() => _count = 0);

  @override
  Widget build(BuildContext context) {
    return Column(mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$_count', style: const TextStyle(fontSize: 64)),
        Row(mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(onPressed: _decrement, child: const Text('−')),
            SizedBox(width: 16),
            ElevatedButton(onPressed: _reset, child: const Text('Reset')),
            SizedBox(width: 16),
            ElevatedButton(onPressed: _increment, child: const Text('+')),
          ],
        ),
      ],
    );
  }
}

Real-World Example: Dynamic Toggle Switch

settings_screen.dart
class SettingsScreen extends StatefulWidget {
  const SettingsScreen({super.key});

  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  bool _notifications = true;
  bool _darkMode = false;
  bool _biometrics = false;

  @override
  Widget build(BuildContext context) {
    return ListView(children: [
      SwitchListTile(
        title: const Text('Notifications'),
        value: _notifications,
        onChanged: (v) => setState(() => _notifications = v),
      ),
      SwitchListTile(
        title: const Text('Dark Mode'),
        value: _darkMode,
        onChanged: (v) => setState(() => _darkMode = v),
      ),
      SwitchListTile(
        title: const Text('Biometric Login'),
        value: _biometrics,
        onChanged: (v) => setState(() => _biometrics = v),
      ),
    ]);
  }
}

Summary

  • StatelessWidget = immutable, no internal state
  • StatefulWidget = mutable, maintains State object
  • initState()build()dispose() is the core lifecycle
  • setState() triggers rebuild — keep it minimal
🚀 Next Step Continue to Building Complex UIs to master layout widgets.
K
Kesavaraja Murugesan
Flutter Developer & Educator

Flutter developer with 3 years of experience building production-grade mobile applications. Passionate about teaching clean architecture patterns and helping developers write maintainable, scalable code.

← Architecture Next: Complex UIs →