Flutter for Beginners: State Basics with StatefulWidget and setState

Capítulo 8

Estimated reading time: 8 minutes

+ Exercise

StatelessWidget vs StatefulWidget: what changes and what doesn’t

In Flutter, the UI is rebuilt often. The key question is: does this widget need to remember something that can change while the app is running?

StatelessWidget

  • Use when the widget’s output depends only on the incoming configuration (constructor parameters) and inherited data (like Theme).
  • No mutable state stored inside the widget instance.
  • If something changes, the parent must rebuild it with new inputs.

StatefulWidget

  • Use when the widget needs to hold mutable values that change over time (counter value, toggle on/off, selected tab, loading flag).
  • A StatefulWidget is paired with a separate State object that holds the mutable fields.
  • When state changes, you call setState to tell Flutter to rebuild that widget subtree.

How setState triggers rebuilds (and what it actually does)

setState(() { ... }) does two things:

  • Runs your callback immediately, where you update your state variables.
  • Marks the current State object as “dirty”, scheduling a rebuild for the next frame. Flutter then calls build again and updates what changed.

setState does not “redraw the whole app”; it rebuilds the widget subtree under that State. Flutter then efficiently updates the rendered output.

Refactor a static UI into an interactive one: Counter

Start with a static screen that shows a number but doesn’t change. This is a common first refactor from StatelessWidget to StatefulWidget.

Step 1: Static version (StatelessWidget)

import 'package:flutter/material.dart';

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: const Center(
        child: Text(
          '0',
          style: TextStyle(fontSize: 48),
        ),
      ),
    );
  }
}

This is fine if the number never changes. But a counter is meant to change based on user actions, so we need state.

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

Download the app

Step 2: Convert to StatefulWidget

In a StatefulWidget, the widget class is mostly a shell; the mutable fields live in the State class.

import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Text(
          '$_count',
          style: const TextStyle(fontSize: 48),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _count++;
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

What changed?

  • _count is stored in the State object.
  • When the button is pressed, we update _count inside setState.
  • Flutter calls build again, and the text shows the new value.

Step 3: Counter variations (increment, decrement, reset)

As the UI grows, keep the state updates small and predictable. A good pattern is to move logic into methods and call them from handlers.

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  void _decrement() {
    setState(() {
      _count--;
    });
  }

  void _reset() {
    setState(() {
      _count = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter Variations')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('$_count', style: const TextStyle(fontSize: 48)),
            const SizedBox(height: 16),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                IconButton(onPressed: _decrement, icon: const Icon(Icons.remove)),
                const SizedBox(width: 12),
                IconButton(onPressed: _increment, icon: const Icon(Icons.add)),
                const SizedBox(width: 12),
                OutlinedButton(onPressed: _reset, child: const Text('Reset')),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

This keeps build focused on describing UI, while methods handle state transitions.

Refactor a static UI into an interactive one: Toggle

Toggles are a great example of boolean state. Start with a label that always says “Off”, then make it interactive.

Step-by-step: Switch with state

import 'package:flutter/material.dart';

class TogglePage extends StatefulWidget {
  const TogglePage({super.key});

  @override
  State<TogglePage> createState() => _TogglePageState();
}

class _TogglePageState extends State<TogglePage> {
  bool _isOn = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Toggle')),
      body: Center(
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Switch(
              value: _isOn,
              onChanged: (value) {
                setState(() {
                  _isOn = value;
                });
              },
            ),
            const SizedBox(width: 12),
            Text(_isOn ? 'On' : 'Off'),
          ],
        ),
      ),
    );
  }
}

Notice the pattern: the widget reads from state (value: _isOn), and user interaction updates state (onChanged calls setState).

Refactor a static UI into an interactive one: Favorite button

A “favorite” (heart/star) is another boolean state, but it also demonstrates how state can affect styling and icons.

Step-by-step: Icon toggle

import 'package:flutter/material.dart';

class FavoriteCard extends StatefulWidget {
  const FavoriteCard({super.key});

  @override
  State<FavoriteCard> createState() => _FavoriteCardState();
}

class _FavoriteCardState extends State<FavoriteCard> {
  bool _isFavorite = false;

  void _toggleFavorite() {
    setState(() {
      _isFavorite = !_isFavorite;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Favorite')),
      body: Center(
        child: Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                IconButton(
                  onPressed: _toggleFavorite,
                  icon: Icon(
                    _isFavorite ? Icons.favorite : Icons.favorite_border,
                    color: _isFavorite ? Colors.red : null,
                  ),
                ),
                const SizedBox(width: 8),
                Text(_isFavorite ? 'Saved' : 'Save'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

This is the same state pattern again, which is a good sign: once you understand setState, many interactive UIs become straightforward.

What should be stored in state (and what shouldn’t)

Store in state only what must persist across rebuilds and can change over time.

Good candidates for state

  • User-driven values: counters, toggles, selected index, expanded/collapsed flags.
  • UI status: loading flags, error flags, “is submitting” booleans.
  • Temporary input-related values when needed by the widget: e.g., a selected date, a filter choice.

Usually not state

  • Values that can be derived from other state: if you can compute it in build, don’t store a duplicate.
  • Constants and styling: keep them as const widgets or final values.
  • Data that belongs higher in the tree: if multiple widgets need the same value, lifting state up is often better than duplicating it.

Example of derived value: instead of storing both _count and _isEven, store only _count and compute final isEven = _count.isEven; in build.

Keep build methods clean

build should be easy to scan: it describes UI, not business logic. A few practical techniques help a lot.

1) Move state changes into methods

Instead of writing complex logic inside button handlers, create methods like _increment, _toggleFavorite, _reset. This makes the widget easier to test and maintain.

2) Extract widgets for repeated UI

If a section of UI grows, extract it into a separate widget. If it doesn’t need to manage its own state, make it a StatelessWidget and pass values/callbacks in.

class CounterControls extends StatelessWidget {
  final VoidCallback onDecrement;
  final VoidCallback onIncrement;
  final VoidCallback onReset;

  const CounterControls({
    super.key,
    required this.onDecrement,
    required this.onIncrement,
    required this.onReset,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        IconButton(onPressed: onDecrement, icon: const Icon(Icons.remove)),
        IconButton(onPressed: onIncrement, icon: const Icon(Icons.add)),
        OutlinedButton(onPressed: onReset, child: const Text('Reset')),
      ],
    );
  }
}

Your stateful page stays focused on state, while the extracted widget focuses on layout.

3) Prefer const where possible

Mark widgets as const when their inputs are compile-time constants. This reduces rebuild work and clarifies what is truly dynamic.

Common mistakes with setState (and how to avoid them)

1) Updating state during build

Never call setState inside build. It can create an infinite rebuild loop because setState schedules another build.

Problematic pattern:

@override
Widget build(BuildContext context) {
  setState(() {
    _count = 0;
  });
  return Text('$_count');
}

Fix: initialize state in initState (for one-time setup) or update state in response to user actions and async callbacks.

2) Mutating state without setState

If you change a state field but don’t call setState, the UI won’t rebuild, so the screen won’t reflect the new value.

void _incrementWrong() {
  _count++; // UI will not update
}

Fix:

void _increment() {
  setState(() {
    _count++;
  });
}

3) Doing expensive work inside setState

setState should be small: update fields only. Heavy computation should happen outside, then you store the result inside setState.

void _updateWithWork() {
  final computed = List.generate(100000, (i) => i).reduce((a, b) => a + b);
  setState(() {
    _count = computed;
  });
}

4) Calling setState after dispose

If you start an async operation and the user leaves the screen before it finishes, calling setState afterward can throw an error because the State is no longer mounted.

Future<void> _load() async {
  await Future.delayed(const Duration(seconds: 2));
  if (!mounted) return;
  setState(() {
    _isOn = true;
  });
}

5) Storing UI objects in state

Avoid storing widgets, colors, or text widgets as state. Store the minimal data (like bool _isFavorite) and let build decide which widget to show.

Mental model: state drives UI

When using StatefulWidget, aim for this flow:

  • State variables represent the current UI situation.
  • The build method reads state and returns widgets.
  • User actions (or async results) update state inside setState.
  • Flutter rebuilds, and the UI reflects the new state.

Now answer the exercise about the content:

In a StatefulWidget, what is the main effect of calling setState(() { ... }) after updating a state variable like _count?

You are right! Congratulations, now go to the next page

You missed! Try again.

setState runs your callback immediately to change state fields, then marks the current State as dirty so Flutter rebuilds that widget subtree on the next frame.

Next chapter

Flutter for Beginners: Simple State Management with InheritedWidget or Provider Concepts

Arrow Right Icon
Free Ebook cover Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch
67%

Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.