Kodokon kodokon.com

Interactivity: buttons, StatefulWidget, and setState

Bring your screens to life by responding to button taps and updating the display with setState.

10 min · 3 questions

Open this lesson in Kodokon

Our screens are pretty but frozen: it's time to respond to the user's gestures. The standard Material 3 button is ElevatedButton. It takes two essential parameters: child, its displayed content (often a Text), and onPressed, the function to run when you tap it. A function passed in this way as a parameter is called a callback: you don't call it yourself, Flutter calls it for you at the right moment. The () { ... } syntax creates an anonymous function, with no name, written directly on the spot.

DART
import 'package:flutter/material.dart';

Widget buildHelloButton() {
  return ElevatedButton(
    onPressed: () {
      debugPrint("Button pressed!");
    },
    child: const Text("Tap here"),
  );
}
A button that writes to the console on each tap

debugPrint writes to the console... but the screen itself doesn't change. Why? Because a StatelessWidget (a stateless widget) is frozen: once built, it can no longer change. To display a value that evolves, you need a StatefulWidget (a stateful widget). The state, that's the set of data that can change during the widget's life: a counter, a checked box, some typed text... A StatefulWidget always goes hand in hand with a State class, which keeps this data and contains the build method. Here's the great classic of the early days: the counter.

DART
import 'package:flutter/material.dart';

void main() {
  runApp(const CounterApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const CounterPage(),
    );
  }
}

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(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              "Counter: $count",
              style: const TextStyle(fontSize: 32),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  count = count + 1;
                });
              },
              child: const Text("+1"),
            ),
          ],
        ),
      ),
    );
  }
}
The counter: a complete StatefulWidget

The mechanism comes in three steps. 1) The count variable lives in _CounterPageState: that's the state. 2) On each tap, the callback calls setState: this method modifies the state and tells Flutter that something has changed. 3) Flutter then re-runs build, which redraws the screen with the new value of count. Remember the philosophy: you never change the screen directly, you change the data, and Flutter takes care of the display.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What is the onPressed parameter of an ElevatedButton for?
    • To set the text shown on the button
    • To provide the function run when the button is tapped
    • To choose the button's color
  2. When should you use a StatefulWidget rather than a StatelessWidget?
    • When the display depends on data that changes over time
    • When the widget contains more than three children
    • When you want the app to start faster
  3. What happens if you change count without calling setState?
    • The app crashes immediately
    • The variable changes in memory but the screen isn't updated
    • Flutter calls setState automatically for you