Flutter Stateless vs Stateful Widgets

Every Flutter widget is either stateless or stateful. This distinction determines whether a widget can change its appearance while the app is running. Choosing the right type is a core Flutter skill.

What Is State

State is data that can change over time and affects what the user sees. A counter value, a checkbox tick, a selected color — these are all examples of state.

  No state change needed:
  ┌─────────────────────────────────────┐
  │  "Welcome to our app"               │  → StatelessWidget
  │  (always shows the same text)       │
  └─────────────────────────────────────┘

  State changes needed:
  ┌─────────────────────────────────────┐
  │  Counter: [0]  [+]  [-]             │  → StatefulWidget
  │  (number changes when buttons tap)  │
  └─────────────────────────────────────┘

StatelessWidget

A StatelessWidget builds its UI once. It cannot update itself — if the parent widget rebuilds, the stateless widget rebuilds with it, but it holds no internal data of its own.

class ProfileCard extends StatelessWidget {
  final String name;
  final String role;

  const ProfileCard({required this.name, required this.role});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          Text(name, style: TextStyle(fontSize: 20)),
          Text(role, style: TextStyle(color: Colors.grey)),
        ],
      ),
    );
  }
}
  Lifecycle:
  ──────────────────────────────────────────
  Create → build() → Draw → (parent changes?) → Rebuild

StatefulWidget

A StatefulWidget splits into two parts: the widget class itself (immutable) and a State class (mutable). The State class holds the data that can change.

class CounterWidget extends StatefulWidget {
  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;  // This is the state

  void _increment() {
    setState(() {
      _count++;     // Tell Flutter to rebuild
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count', style: TextStyle(fontSize: 32)),
        ElevatedButton(
          onPressed: _increment,
          child: Text('Add'),
        ),
      ],
    );
  }
}

The Role of setState()

setState() is the key method. It does two things: updates the variable value and tells Flutter to re-run the build() method so the screen refreshes.

  Without setState():
  _count++;          → Variable updates in memory
                     → Screen stays the same (stale)

  With setState():
  setState(() {
    _count++;        → Variable updates
  });               → Flutter calls build() again
                    → Screen shows new value ✓

StatefulWidget Lifecycle

The State class goes through several lifecycle stages you need to know about.

  ┌──────────────────────────────────────────────────┐
  │               LIFECYCLE FLOW                     │
  │                                                  │
  │  createState()                                   │
  │       ↓                                          │
  │  initState()   ← runs once on creation           │
  │       ↓                                          │
  │  build()       ← runs every time state changes   │
  │       ↓                                          │
  │  setState()    ← triggers rebuild                │
  │       ↓                                          │
  │  build()       ← runs again                      │
  │       ↓                                          │
  │  dispose()     ← runs when widget is removed     │
  └──────────────────────────────────────────────────┘

initState() — Run Code on Start

@override
void initState() {
  super.initState();
  // Runs once when widget is first created
  // Good for: fetching data, starting timers
  fetchUserData();
}

dispose() — Clean Up

@override
void dispose() {
  // Runs when widget is removed from screen
  // Good for: cancelling timers, closing streams
  _timer.cancel();
  super.dispose();
}

Choosing Between Stateless and Stateful

Use StatelessWidget when…Use StatefulWidget when…
Content is fixed (headings, labels, icons)Content changes after user interaction
Data comes from parent onlyWidget manages its own data
No buttons or input fields that trigger changesInvolves counters, toggles, form inputs
Performance-critical, frequently rebuilt areasLoading spinners that show/hide

Practical Example — Toggle Button

class FavoriteButton extends StatefulWidget {
  @override
  State<FavoriteButton> createState() => _FavoriteButtonState();
}

class _FavoriteButtonState extends State<FavoriteButton> {
  bool _isFavorite = false;

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(
        _isFavorite ? Icons.favorite : Icons.favorite_border,
        color: _isFavorite ? Colors.red : Colors.grey,
      ),
      onPressed: () {
        setState(() {
          _isFavorite = !_isFavorite;
        });
      },
    );
  }
}
  Tapped OFF:  ♡ (grey border heart)
  Tapped ON:   ♥ (red filled heart)
  Toggle back: ♡ (grey border heart)

Performance Tip

Keep StatefulWidgets as small as possible. If only a small part of your screen changes, extract that part into its own StatefulWidget. This way Flutter only rebuilds that small piece instead of the whole screen.

Leave a Comment

Your email address will not be published. Required fields are marked *