🔄 Stateful Widgets — Make Your App Come Alive!
🧒 Why Won't My App Update? (The Magic Painting Analogy!)
Your App Is Currently a "Painting" — Not a "TV Screen"
Right now, your Birdle app is like a beautiful painting on the wall. You can type guesses and press Enter, but the tiles never change. The game logic runs behind the scenes, but the screen doesn't update!
That's because your GamePage is a StatelessWidget —
it's painted once and stays frozen forever. To make it update,
you need to convert it into a StatefulWidget — like turning a
painting into a TV screen that can show new images!
- ✅ You'll understand when widgets need to be stateful
- ✅ You'll convert GamePage from StatelessWidget to StatefulWidget
- ✅ You'll use
setStateto update the UI when data changes - ✅ Your Birdle tiles will actually change colors when you guess!
StatelessWidget vs StatefulWidget — The Key Difference
StatelessWidget
Like a painting. Once it's drawn, it never changes. All the data comes from outside (constructor parameters).
Example: Your Tile widget — given a letter and hitType, it always looks the same.
StatefulWidget
Like a TV screen. It can update and show new content when data changes. It has a memory (called State).
Example: Your GamePage — needs to show new guesses and change tile colors.
A StatefulWidget Has TWO Classes Working Together
Unlike a StatelessWidget (which is just one class), a StatefulWidget is actually two classes that work as a team:
1. The Widget (GamePage)
The ID card — it tells Flutter: "I'm a GamePage widget." It's still immutable (can't change) and just creates the State object.
class GamePage extends StatefulWidget
2. The State (_GamePageState)
The brain and memory — this is where all the changeable data lives. It has the build method and can call setState to update the UI.
class _GamePageState extends State<GamePage>
The underscore _ before _GamePageState makes it private — only the GamePage widget can use its own State. No one else can mess with it!
1 The Basic StatefulWidget Pattern
Before we convert GamePage, let's see the bare-bones structure of a StatefulWidget so you recognize the pattern.
Step 1.1 The Minimum StatefulWidget Code
Here's the simplest possible StatefulWidget — it doesn't do anything yet, but it shows the structure:
🔍 Anatomy of a StatefulWidget
// PART 1: The Widget (immutable ID card)
class ExampleWidget extends StatefulWidget {
ExampleWidget({super.key});
@override
State<ExampleWidget> createState() => _ExampleWidgetState();
// ↑ This is the ONLY job of the widget class:
// create and return a State object
}
// PART 2: The State (brain with memory)
class _ExampleWidgetState extends State<ExampleWidget> {
// ↑ Underscore _ = private. Only ExampleWidget can use this.
@override
Widget build(BuildContext context) {
return Container(); // Your UI goes here
}
}
Key things to notice:
- Two classes — The widget class and the state class. They're a team!
createState()— The widget's only job. It returns the State object.build()— Now lives in the State class, not the widget class.- The underscore
_— Makes the State class private to this file. - The State class uses
State<ExampleWidget>— it's tied to its widget.
2 Convert GamePage to a StatefulWidget
Now let's actually convert your GamePage. Follow these steps carefully — this is the most important transformation in the whole course!
Step 2.1 BEFORE: Your Current StatelessWidget GamePage
Here's what your GamePage probably looks like right now (or similar):
class GamePage extends StatelessWidget { // ← STATELESS
GamePage({super.key});
final Game _game = Game(); // ← Game logic (mutable data!)
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
for (var guess in _game.guesses)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var letter in guess)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 2.5, vertical: 2.5),
child: Tile(letter.char, letter.type),
)
],
),
GuessInput(
onSubmitGuess: (guess) {
print(guess); // Temporary!
},
),
],
),
);
}
}
Step 2.2 AFTER: Converted to StatefulWidget
Here's the exact same widget, converted to a StatefulWidget. Compare it line by line:
// PART 1: The Widget (just an ID card now)
class GamePage extends StatefulWidget { // ← CHANGED to StatefulWidget
GamePage({super.key});
@override
State<GamePage> createState() => _GamePageState(); // ← NEW: creates State
}
// PART 2: The State (brain with memory)
class _GamePageState extends State<GamePage> { // ← NEW: State class
final Game _game = Game(); // ← MOVED from widget to State
@override
Widget build(BuildContext context) { // ← MOVED to State class
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
for (var guess in _game.guesses)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var letter in guess)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 2.5, vertical: 2.5),
child: Tile(letter.char, letter.type),
)
],
),
GuessInput(
onSubmitGuess: (guess) {
print(guess); // Still temporary! We'll fix this next.
},
),
],
),
);
}
}
What changed (3 things):
- GamePage now extends StatefulWidget — It's no longer frozen.
- Added createState() method — Returns a new _GamePageState object.
- Created _GamePageState class — Contains the Game object and the build method. Everything that was in the widget class moved here.
StatelessWidget,
press Ctrl + . (or Cmd + . on Mac), and select "Convert to StatefulWidget".
VS Code does the whole conversion automatically! But it's important to understand what's happening,
so try doing it manually first.
3 Why Does This Matter? — The setState Magic
Converting to StatefulWidget alone doesn't fix anything. The real power comes from setState — the method that tells Flutter: "Hey! Something changed! Redraw the screen!"
Step 3.1 The Problem: Data Changes, But Screen Doesn't
Imagine this scenario without setState:
onSubmitGuess: (guess) {
_game.guess(guess); // ← Game data changes internally...
// BUT Flutter doesn't know it needs to redraw!
// The screen stays frozen. User sees nothing happen.
},
This is like changing the channel on your TV but never turning the screen on. The channel changed, but you can't see it!
Step 3.2 The Solution: Wrap Changes in setState()
Now wrap the game logic in setState():
onSubmitGuess: (String guess) {
setState(() { // ← "Hey Flutter! Watch me change stuff!"
_game.guess(guess); // ← Actually evaluate the guess
}); // ← Flutter now redraws the screen!
},
What setState does (in plain English):
- Runs your code — It executes whatever function you give it (like
_game.guess(guess)). - Marks the widget as "dirty" — Tells Flutter: "This widget's data changed. It needs to be redrawn."
- Schedules a rebuild — Flutter calls your
build()method again, and this time the UI reflects the new data.
onSubmitGuess callback to use setState as shown above.
Hot reload, type a legal 5-letter word (like "aback" or "abbey"), and press Enter.
The tiles should now show your guess with colors! 🎉
- Only change state variables inside setState (things that affect the UI).
- Don't do heavy work (like HTTP requests) inside setState — it should be fast.
- Never call setState inside
build()— that creates an infinite loop! - setState only affects the widget that called it and its children.
4 Visualizing the setState Flow
Let's trace exactly what happens when a user submits a guess, from keyboard press to screen update.
Step 4.1 The Complete Flow — Step by Step
- User types "ABACK" and presses Enter
- GuessInput.onSubmitted fires → calls
onSubmitGuess("aback") - onSubmitGuess runs setState() → tells Flutter "watch this!"
- _game.guess("aback") executes → game logic evaluates each letter:
- If 'a' matches → HitType.hit (green)
- If 'b' is in word but wrong spot → HitType.partial (yellow)
- If 'c' isn't in word → HitType.miss (gray)
- Game saves the guess in its internal list
- setState finishes → Flutter marks GamePage as "dirty"
- Flutter calls build() again → the for-loop reads _game.guesses
- Tiles are created with new colors → screen updates!
📝 What You Learned Today
You just learned the most important concept for making interactive Flutter apps!
When to Use StatefulWidget
Use StatefulWidget when your widget's appearance or data needs to change during its lifetime. Start with StatelessWidget and convert only when needed.
Converted GamePage
Refactored GamePage into two classes: GamePage (the widget) and _GamePageState (the state with memory and build method).
Used setState to Update UI
Called setState(() { _game.guess(guess); }) — Flutter now knows to rebuild the widget tree and show new tile colors.
Your App is Now Interactive!
Users can type guesses, submit them, and see the tiles update with the correct colors. The Birdle game works!
🧠 Test Yourself!
Let's check your understanding of StatefulWidgets:
Q1 When should you use a StatefulWidget instead of a StatelessWidget?
Q2 What happens if you change data in a State object without calling setState?
📦 Project 1: Build a Tap Counter App
Practice StatefulWidget and setState by building a classic counter app from scratch!
Objective: Create a Button That Counts Taps
📋 Requirements:
- Create a new StatefulWidget called
CounterPage - It should have a State class with an
int _count = 0;variable - The build method should return a Column with:
- A Text widget showing: "You've tapped the button X times" (where X is _count)
- A SizedBox(height: 20)
- An ElevatedButton that says "Tap me!"
- When the button is pressed, call
setState(() { _count++; }) - Center the Column on screen
🎯 Expected Output:
A centered counter that increments every time you tap the button. The text updates automatically!
onPressed: () { setState(() { _count++; }); } — without setState, the count changes but the screen never updates!
📦 Project 2: Build a Color-Changing Container
Use setState to dynamically change a widget's appearance — a pattern you'll use constantly!
Objective: Tap to Change Background Color
📋 Requirements:
- Create a StatefulWidget called
ColorChanger - It should have a List of Colors:
[Colors.red, Colors.blue, Colors.green, Colors.orange, Colors.purple] - Have an
int _currentIndex = 0;state variable - The build method should return:
- A Container with width: 200, height: 200
- The Container's color should be
_colors[_currentIndex] - Add
child: Center(child: Text('Tap me!'))inside the Container
- Wrap the Container in a GestureDetector — when tapped, call
setStateto increment_currentIndex(use% _colors.lengthto cycle through colors)
🎯 Expected Output:
A colored square that cycles through red → blue → green → orange → purple → red... each time you tap it.
- Use
GestureDetector(onTap: () { setState(() { _currentIndex = (_currentIndex + 1) % _colors.length; }); }, child: Container(...)) - The
%(modulo) operator wraps around — when index reaches 5, it goes back to 0 - Use
BoxDecoration(color: _colors[_currentIndex])instead of the Container's color property if you want to add border radius
Lesson Complete!
You converted your app to use StatefulWidget and setState — the foundation of ALL interactive Flutter apps. Your Birdle game now responds to user guesses with colored tiles!
Click the button above to track your progress!