Flutter State Management for Beginners: Build a Counter App and Understand setState()

Flutter State Management for Beginners Build a Counter App and Understand setState()

Welcome! Let’s Build Something Awesome Together. Have you ever wondered how apps remember things?

Think about your favorite social media app. When you tap the “Like” button, the heart turns red, and the like count instantly goes up by one. When you add an item to a shopping cart, the cart icon immediately updates to show your total.

How does the app know that something changed, and how does it update the screen so quickly?

The secret ingredient behind all of this is called State.

If you are new to Flutter, the word “State” might sound a bit technical or intimidating. But don’t worry! In this guide, we are going to break it down together step-by-step.

Instead of just reading boring theories, we are going to build a practical Counter App from scratch.

By the time you finish this short guide, you won’t just understand how state works—you will have built a working app that can increase, decrease, and reset numbers at the tap of a button.

Ready to build your first dynamic Flutter app? Let’s get started!

What is State in Flutter?

Before we look at any code, let’s answer a simple question: What exactly is “State”?

In plain English, State is the information that your app remembers at any given moment. Think of your app like a person.

Right now, you might be happy, sitting down, and drinking a cup of coffee. If you stand up, your position changes. If you finish your coffee, your cup becomes empty.

Your current mood, your position, and how much coffee you have left are all pieces of your current “state.”

In a mobile app, it works exactly the same way. State is everything that can change while someone is using your app.

Here are a few quick examples you see every day:

  • A checkbox: Is it checked (True) or unchecked (False)?
  • A login screen: Is the user logged in, or are they still typing their password?
  • A shopping cart: How many items are inside right now?

In Flutter, the user interface (the UI) is built directly from this state. When the state changes, the screen updates to reflect the new information. It’s that simple!

Why Do We Need State Management?

Let’s tackle Why Do We Need State Management? by showing them the “problem” it solves, keeping the tone helpful and relatable.

Why Do We Need State Management?

Now that we know what state is, you might wonder: Why do we need a system to manage it? Can’t we just change a variable and call it a day?

To understand why we need state management, let’s look at how Flutter actually draws things on your screen.

Flutter is incredibly fast because it builds your user interface (UI) like a blueprint based on your data. But here is the catch: once Flutter draws a widget on the screen, that widget is immutable—which is just a fancy way of saying it cannot change itself.

Imagine you write a number on a piece of paper with a permanent marker. If you want to change that number from 0 to 1, you can’t just rewrite it on top.

It will look like a messy blob! Instead, you have to crumple up that piece of paper, throw it away, and write 1 on a brand-new sheet.

This is exactly why we need State Management. It acts as the manager of your app that:

  1. Watches for changes: It notices the exact moment a user taps a button or types some text.
  2. Updates the data: It changes the variable behind the scenes (like turning a 0 into a 1).
  3. Tells Flutter to redraw: It politely tells Flutter, “Hey, the data changed! Please throw away the old screen and quickly redraw a fresh one with the new numbers.”

Without state management, your app’s data might change in the background, but the screen would stay completely frozen. State management is the bridge that keeps your data and your user interface perfectly in sync.

Understanding the Counter App

Before we open our code editor and start building, let’s take a quick look at exactly what we are going to create.

We are going to build an interactive Counter Application. While a counter might look simple on the surface, it is actually the perfect project to learn state management because it uses the exact same core logic that powers massive apps like Instagram, Uber, or Amazon.

Here is how our app will work when we are done:

  • The Display: Right in the center of the screen, there will be a large number showing the current count. It will start at 0.
  • The Action Buttons: Instead of just one button, our app will have three distinct actions:
    1. Increment (+): Tapping this will add 1 to our counter.
    2. Decrement (-): Tapping this will subtract 1 from our counter.
    3. Reset (🔄): Tapping this will instantly bring our counter back to 0.

By focusing on these three simple operations, you will see exactly how data moves around in Flutter, how buttons trigger changes, and how the screen updates dynamically.

Now that we have the blueprint in our minds, let’s move on to the fun part: creating the project!

Creating the Project

Let’s roll up our sleeves and create our brand-new Flutter project! We want a clean slate so we can build our counter logic from scratch.

Follow these simple steps to get your project ready:

Step 1: Open Your Terminal or Command Prompt

Open the terminal app on your computer. Navigate to the folder where you like to keep your development projects (for example, your Desktop or Projects folder) by using the cd command:

cd Desktop

Step 2: Run the Flutter Create Command

Type the following command to create a fresh project named simple_counter. Press Enter:

flutter create simple_counter

Flutter will work its magic for a few seconds and set up all the folders and files we need for an Android and iOS application.

Step 3: Open the Project in Your Editor

Navigate into your new project folder:

cd simple_counter

Now, open this folder in your favorite code editor (like VS Code or Android Studio). If you are using VS Code, you can open it directly from the terminal by typing:

code .

Step 4: Clean Up main.dart

When you open the project, look at the folder structure on the left side. Go to the lib folder and open the main.dart file.

Flutter automatically generates a lot of template code and comments here. To make sure we understand every single line of our app, let’s select everything inside main.dart (Ctrl+A or Cmd+A) and delete it completely.

Leave the file totally blank for now. In the next section, we will start building our user interface line by line!

Building the User Interface

Now that we have a blank main.dart file, let’s build the visual structure of our app. We want a beautiful, clean layout with our text display in the middle and our three action buttons arranged neatly at the bottom.

Step 1: The App Entry Point (main)

Every single Flutter app begins at a function called main(). Think of this as the engine starter for your application.

Open your blank main.dart file and add these first few lines:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}
Code language: JavaScript (javascript)

Let’s break down exactly what is happening here:

  • import '...';: This brings in Flutter’s built-in design toolkit (called Material Design). It gives us access to pre-made visual blocks like buttons, text, and colors.
  • void main(): This is the starting gun. When your Android phone launches the app, it looks for this exact name first.
  • runApp(): This is a built-in Flutter function that says, “Hey, take my root widget and display it on the screen.”
  • const MyApp(): This is the name of our main app container. Right now, your code editor might show a red underline here—that’s completely normal because we haven’t built MyApp yet!

Step 2: Creating the Root Widget (MyApp)

Now, let’s fix that red underline by creating our main app container, MyApp.

Add this code directly below your main() function:

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp();
  }
}
Code language: JavaScript (javascript)

Let’s break down what this does:

  • StatelessWidget: In Flutter, almost everything is a “Widget” (a visual building block). A StatelessWidget is a static frame. It sets up the basic structure of our app but doesn’t change its own data over time.
  • @override Widget build(...): This is a configuration window where you describe what your widget should look like. It returns a widget layout to Flutter so it can be drawn on the phone screen.
  • MaterialApp: This is a powerful wrapper widget. It acts as the backbone of our application, setting up the underlying design theme, core fonts, and navigation setup for our Android application.

Step 3: Configuring the MaterialApp

Now, let’s look inside that MaterialApp widget and configure it. Instead of leaving it blank, we will add some properties to give our app a beautiful theme and point it to our main screen.

Update your MaterialApp inside the build method so it looks like this:

return MaterialApp(
  title: 'Simple Counter',
  debugShowCheckedModeBanner: false,
  theme: ThemeData(
    useMaterial3: true,
    colorSchemeSeed: Colors.blue,
    brightness: Brightness.light,
  ),
  home: const HomeScreen(),
);
Code language: JavaScript (javascript)

Let’s see what each of these settings does:

  • title: This is the internal name of your application that the phone’s operating system uses (for example, when you switch between running apps).
  • debugShowCheckedModeBanner: false: This simply hides the small red “Debug” banner from the top corner of your screen, keeping your application looking clean and professional.
  • theme: ThemeData(...): This is where we control the looks of our app.
    • useMaterial3: true enables Google’s latest, modern design system.
    • colorSchemeSeed: Colors.blue sets up a beautiful, consistent blue palette for our buttons and bars.
    • brightness: Brightness.light ensures our app starts in light mode.
  • home: const HomeScreen(): This tells Flutter exactly which screen to open first. Don’t worry if your code editor shows a red underline under HomeScreen—we are going to build that screen together in the next step!

Step 4: Introducing the StatefulWidget (HomeScreen)

Now, let’s fix that red underline by building our HomeScreen. This time, we are using a StatefulWidget instead of a StatelessWidget because this screen needs to remember and update data (our counter number) dynamically.

Add this code at the bottom of your main.dart file:

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold();
  }
}
Code language: JavaScript (javascript)

Let’s break down this new structure:

  • StatefulWidget: This is a special type of widget that can track changes while the app is running. It is split into two parts: the widget itself (HomeScreen) and its companion state class (_HomeScreenState).
  • createState(): This tells Flutter to create a separate space in the phone’s memory to keep track of this screen’s changing data.
  • _HomeScreenState: This is where the magic happens! Any variables, custom logic, or button actions we create will live inside this class.
  • Scaffold(): Inside the build method, we return a blank Scaffold. This gives us a fresh white canvas to start adding our layout elements, like top bars and buttons.

Step 5: Setting Up the Screen Body

Now let’s replace our completely blank Scaffold with some initial layout pieces. We want to make sure our text is safe from overlapping with phone camera notches and has a bit of breathing room around the edges.

Update the build method inside your _HomeScreenState to look like this:

return Scaffold(
  body: SafeArea(
    child: Padding(
      padding: EdgeInsets.all(16),
      child: Text('Simple Counter'),
    ),
  ),
);
Code language: JavaScript (javascript)

Step 6: Centering Content and Stacking Layouts

Instead of just showing a single line of text in the corner, we want our layout ready to hold multiple items stacked on top of each other. We will use a Center widget to position things perfectly, and a Column to stack our text and numbers vertically.

Update the child: of your Padding widget to look like this:

child: Padding(
  padding: EdgeInsets.all(16),
  child: Center(
    child: Column(
      children: [
        Text(
          'Your Counter:',
          style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
        ),
      ],
    ),
  ),
),
Code language: JavaScript (javascript)

Let’s break down this structural layout change:

  • Center: This takes its child widget and perfectly anchors it right in the middle of the phone screen horizontally and vertically.
  • Column: This is a powerful layout widget. It takes a list of multiple widgets inside its children: [...] block and stacks them vertically, one underneath the other.
  • mainAxisAlignment: MainAxisAlignment.center: (Optional tip) Adding this inside your column tells Flutter to gather all the children and keep them tightly centered in the middle of the vertical axis.
  • style: TextStyle(...): This lets us format our text. We increase the fontSize to 16 so it’s easily readable and apply a bold weight to make it act as a clear section header.

Step 7: Adding the Counter Number Display

Now that our vertical stack (Column) is set up, let’s add the star of the show: the big number that will track our count. We will place it directly underneath our header text.

Update your children: [...] list inside the Column to look like this:

children: [
  Text(
    'Your Counter:',
    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  ),
  Text(
    '0',
    style: TextStyle(
      fontSize: 80,
      color: Colors.blue,
      fontWeight: FontWeight.w100,
    ),
  ),
],
Code language: JavaScript (javascript)

Let’s look at the styling choices we made here:

  • Vertical Stacking: Because this new Text widget sits inside the children list right after the “Your Counter:” text, Flutter automatically places it directly beneath it on the screen.
  • fontSize: 80: We want this number to be huge and prominent, making it the clear centerpiece of the app interface.
  • color: Colors.blue: This colors our number with the beautiful blue accent we defined earlier in our app theme.
  • fontWeight: FontWeight.w100: Instead of making the huge number thick and bold, a weight of w100 gives it a sleek, thin, and ultra-modern look that balances perfectly with the large size.

Step 8: Adding the Action Buttons with Wrap

With our display in place, we need a clean way to arrange our interaction buttons below the number. We will use a SizedBox to add some space, and a Wrap widget to hold our three action buttons safely.

Update your children: [...] list inside the Column to include the buttons like this:

children: [
  Text(
    'Your Counter:',
    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  ),
  Text(
    '0',
    style: TextStyle(
      fontSize: 80,
      color: Colors.blue,
      fontWeight: FontWeight.w100,
    ),
  ),
  SizedBox(height: 20),
  Wrap(
    children: [
      ElevatedButton.icon(
        onPressed: () {},
        icon: Icon(Icons.add),
        label: Text('Increment'),
      ),
      ElevatedButton.icon(
        onPressed: () {},
        icon: Icon(Icons.refresh),
        label: Text('Reset'),
      ),
      ElevatedButton.icon(
        onPressed: () {},
        icon: Icon(Icons.remove),
        label: Text('Decrement'),
      ),
    ],
  ),
],
Code language: JavaScript (javascript)

Let’s break down how this section organizes our layout:

  • SizedBox(height: 20): This acts like an invisible spacer block. It pushes the buttons down by 20 pixels so they aren’t uncomfortably crowded against our large number.
  • Wrap: This is a smart layout widget similar to a Row. However, if the screen size is too small (or if the user changes their text size), a Row would break and show a yellow-and-black pixel overflow warning. A Wrap is smart—it automatically flows extra items down to a new line if it runs out of space!
  • ElevatedButton.icon: This is a pre-styled Material Design button that conveniently pairs an icon (like Icons.add, Icons.refresh, or Icons.remove) right next to a text label.
  • onPressed: () {}: The execution blocks are empty for now. If you run the app, the buttons look gorgeous and clickable, but they won’t change the number yet.

Step 9: Adjusting Button Spacing for Breathing Room

To make our buttons look truly polished, we will configure the layout properties of our Wrap widget. By adding explicit horizontal and vertical gaps, we ensure our buttons remain cleanly separated and touch-friendly on any Android device screen.

Update your Wrap widget configuration to include these layout settings:

Wrap(
  runSpacing: 16,
  spacing: 16,
  children: [
    ElevatedButton.icon(
      onPressed: () {},
      icon: Icon(Icons.add),
      label: Text('Increment'),
    ),
    ElevatedButton.icon(
      onPressed: () {},
      icon: Icon(Icons.refresh),
      label: Text('Reset'),
    ),
    ElevatedButton.icon(
      onPressed: () {},
      icon: Icon(Icons.remove),
      label: Text('Decrement'),
    ),
  ],
),
Code language: JavaScript (javascript)

Let’s look at exactly how these spacing rules improve our interface:

  • spacing: 16: This controls the horizontal gap. It places exactly 16 pixels of empty space between the buttons when they sit side-by-side in a row.
  • runSpacing: 16: This controls the vertical gap. If a user has a smaller phone screen or high accessibility text scaling turned on, the buttons will wrap onto a second line. This parameter ensures that the top and bottom rows won’t smash into each other, maintaining a clean layout.

Step 10: Performance Optimization with const

Before we make our application interactive, let’s apply some quick performance upgrades to our code. We will do this by using Flutter’s const keyword on widgets that never change.

Go back through your code and add the const keyword to your padding, static labels, spacers, and button configurations like this:

return Scaffold(
  body: SafeArea(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Center(
        child: Column(
          children: [
            const Text(
              'Your Counter:',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            Text(
              '0',
              style: TextStyle(
                fontSize: 80,
                color: Colors.blue,
                fontWeight: FontWeight.w100,
              ),
            ),
            const SizedBox(height: 20),
            Wrap(
              runSpacing: 16,
              spacing: 16,
              children: [
                ElevatedButton.icon(
                  onPressed: () {},
                  icon: const Icon(Icons.add),
                  label: const Text('Increment'),
                ),
                ElevatedButton.icon(
                  onPressed: () {},
                  icon: const Icon(Icons.refresh),
                  label: const Text('Reset'),
                ),
                ElevatedButton.icon(
                  onPressed: () {},
                  icon: const Icon(Icons.remove),
                  label: const Text('Decrement'),
                ),
              ],
            ),
          ],
        ),
      ),
    ),
  ),
);
Code language: JavaScript (javascript)

Let’s look at why this small change makes a big difference under the hood:

  • What is const?: It stands for constant. It explicitly tells Flutter, “This widget will look exactly the same from the moment the app opens until the user closes it.”
  • The Performance Win: When your app updates the screen to show a new counter number, Flutter has to redraw parts of the user interface. By marking static elements as const, Flutter remembers them in memory and completely skips re-building them, keeping your app running at an ultra-smooth 60 or 120 FPS.

Crucial Detail: Notice that we did not add const to our '0' text widget. That’s because that specific number display is dynamic and will soon change values when a user clicks our buttons!

UI Milestone Achieved! 🏆

You have successfully built the complete visual layout for your counter application. Your widgets are structured, beautifully spaced, and fully performance-optimized.

If you run your application right now, you will see a clean, modern interface sitting right on your screen. However, tapping those buttons won’t alter the display just yet. To make our interface come alive, we need to connect our user actions to actual, live data.

Introducing State

Right now, our application looks great, but it is completely frozen. You can tap the buttons as much as you want, but the screen will stubbornly display 0.

To make our counter actually work, we need to introduce State. In mobile development, “state” is simply any data that can change over time while a user is interacting with your app.

Step 1: Declaring Our State Variable

The first step is to give our app a way to remember the current count. We do this by declaring a variable inside our state class, _HomeScreenState. This variable will live outside our build method so that its value persists.

Open your main.dart file, navigate down to your _HomeScreenState class, and add an integer variable named _counter right at the top:

class _HomeScreenState extends State<HomeScreen> {
  // Declare the state variable here
  int _counter = 0;
Code language: JavaScript (javascript)
  • int _counter = 0;: This creates a plain integer variable and sets its initial starting value to 0.
  • The Underscore (_): In Dart, putting an underscore in front of a variable name marks it as private. This means this specific variable can only be accessed and changed inside this file, protecting our data from accidental edits elsewhere.

Step 2: Displaying the Live Variable

Now that we have a variable holding our number, we need to stop using the hardcoded text string '0' and tell our Text widget to read directly from _counter.

Scroll down to your Column children and update the number display Text widget like this:

// Before: Text('0', ...)
Text(
  '$_counter', // Updated to look at our variable
Code language: JavaScript (javascript)
  • '$_counter': By using the $ symbol inside the quotes, we are telling Flutter to look at our _counter variable, grab its current value, and turn it into text on the screen.

Our variable is declared and wired straight into the user interface!

Why Changing the Variable Alone Doesn’t Work

Now that we have our _counter variable hooked up, a natural next step might be to try updating its value directly when a button is pressed. Let’s look at why this logical guess doesn’t actually work in Flutter.

Imagine we update our Increment button’s onPressed block to look like this:

ElevatedButton.icon(
  onPressed: () {
    _counter++;
    print(_counter);
  },
  icon: const Icon(Icons.add),
  label: const Text('Increment'),
),
Code language: PHP (php)

If you add that line, hot reload your app, and click the button, you will notice a strange behavior:

  1. In your development console, you will see the print() statement outputting 1, 2, 3, 4 perfectly. The variable is changing in the phone’s memory!
  2. On your phone screen, the big blue number stubbornly remains 0.

Why is this happening?

Flutter is an incredibly fast framework because it only redraws things when it is explicitly told to.

When you change a variable like _counter++, you are quietly updating a value in the background memory. However, Flutter has no idea that the value changed. Because it doesn’t know, it never runs the build() method again. If the build() method doesn’t re-run, the user interface is never updated, and the old value stays frozen on the screen.

To fix this, we need a way to tell Flutter: “Hey, we just changed a piece of data. Please redraw the screen right now so the user can see it!”

Understanding setState()

To fix the frozen screen issue, we need to introduce one of the most fundamental tools in Flutter development: the setState() function.

setState() is a built-in method that is only available inside a StatefulWidget. It acts as an explicit trigger or an alarm system for the Flutter framework.

When you wrap code inside setState(), you are telling Flutter two things:

  1. “I am changing a variable that our user interface depends on right now.”
  2. “Please immediately re-run our build() method to redraw the screen with the fresh data.”

How It Works Under the Hood

When a user taps a button and triggers setState(), Flutter executes the code inside its brackets instantly. Once that code finishes running, Flutter marks this specific widget as “dirty” (meaning it needs an update) and schedules a fast UI redraw.

The build() method executes again from top to bottom. When it reaches our Text('$_counter') widget, it reads the newly updated value from memory and draws the new number on the screen at a flawless 60 or 120 frames per second.

Without setState(), your app updates its memory quietly in the dark. With setState(), the user interface stays perfectly in sync with your data.

Implementing Increment

Now that we understand how setState() works, let’s put it to work! We will modify our Increment button’s onPressed property so that every time a user taps it, our counter variable increases by 1 and the screen updates instantly.

Find the first ElevatedButton.icon inside your Wrap widget and update its code to look like this:

ElevatedButton.icon(
  onPressed: () {
    // Wrap our variable update inside setState
    setState(() {
      _counter++;
    });
  },
  icon: const Icon(Icons.add),
  label: const Text('Increment'),
),
Code language: JavaScript (javascript)

What happens when you tap this button?

  1. The Trigger: The user taps the “Increment” button, running the onPressed function.
  2. The Update: Inside setState(), _counter++ fires, changing our value from 0 to 1 in the phone’s memory.
  3. The Redraw: Because it’s wrapped in setState(), Flutter immediately triggers the build() method.
  4. The Result: The screen updates, and the user instantly sees the big blue number jump to 1.

Implementing Decrement

Now let’s configure our third button to do the exact opposite: decrease our counter value by 1 every time it is pressed.

Scroll down to the third ElevatedButton.icon inside your Wrap widget (the one with the Icons.remove icon) and update its code to look like this:

ElevatedButton.icon(
  onPressed: () {
    setState(() {
      _counter--;
    });
  },
  icon: const Icon(Icons.remove),
  label: const Text('Decrement'),
),
Code language: JavaScript (javascript)

Preventing Negative Numbers (Optional Refinement)

If you test your app right now and click Decrement repeatedly when the counter is at 0, the number will roll over into -1, -2, and so on. If you want this to be a strictly positive counter app, we can add a quick safety check using an if statement:

ElevatedButton.icon(
  onPressed: () {
    if (_counter > 0) {
      setState(() {
        _counter--;
      });
    }
  },
  icon: const Icon(Icons.remove),
  label: const Text('Decrement'),
),
Code language: JavaScript (javascript)

By wrapping the logic inside if (_counter > 0), the decrement operation will only execute if the current count is greater than zero. If the counter is 0, the button safely does nothing, keeping your app data reliable and predictable.

Implementing Reset

To finish up our control panel layout, we need to configure our middle button: the Reset button. This button will wipe the slate clean, resetting our counter back to its original starting value of 0 in a single tap.

Scroll to the middle ElevatedButton.icon inside your Wrap widget (the one with the Icons.refresh icon) and update its code to look like this:

ElevatedButton.icon(
  onPressed: () {
    setState(() {
      _counter = 0; // Set the variable back to zero
    });
  },
  icon: const Icon(Icons.refresh),
  label: const Text('Reset'),
),
Code language: JavaScript (javascript)

Breaking It Down

  • _counter = 0;: Instead of adding or subtracting, we use the assignment operator to directly force our variable back to its baseline value.
  • setState(...): Just like before, wrapping this assignment inside setState() tells Flutter to immediately wipe the old number off the screen and redraw the clean 0.

Fantastic! All three of your action buttons are now completely interactive and operational.

Full Source Code

Here is the complete, production-ready code for your fully interactive counter application. You can replace the entire content of your lib/main.dart file with this code:

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Simple Counter',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.blue,
        brightness: Brightness.light,
      ),
      home: const HomeScreen(),
    );
  }
}

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  // Declare the state variable here
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Center(
            child: Column(
              children: [
                const Text(
                  'Your Counter:',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                // Before: Text('0', ...)
                Text(
                  '$_counter', // Updated to look at our variable
                  style: TextStyle(
                    fontSize: 80,
                    color: Colors.blue,
                    fontWeight: FontWeight.w100,
                  ),
                ),
                const SizedBox(height: 20),
                Wrap(
                  runSpacing: 16,
                  spacing: 16,
                  children: [
                    ElevatedButton.icon(
                      onPressed: () {
                        // Wrap our variable update inside setState
                        setState(() {
                          _counter++;
                        });
                      },
                      icon: const Icon(Icons.add),
                      label: const Text('Increment'),
                    ),
                    ElevatedButton.icon(
                      onPressed: () {
                        setState(() {
                          _counter = 0; // Set the variable back to zero
                        });
                      },
                      icon: const Icon(Icons.refresh),
                      label: const Text('Reset'),
                    ),
                    ElevatedButton.icon(
                      onPressed: () {
                        if (_counter > 0) {
                          setState(() {
                            _counter--;
                          });
                        }
                      },
                      icon: const Icon(Icons.remove),
                      label: const Text('Decrement'),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
Code language: JavaScript (javascript)

📂 Grab the Complete Project on GitHub

Want to clone the entire project, explore the folder structure, or double-check your configuration? You can access the complete source code repository right on GitHub.

View Complete Source Code on GitHub

What We Learned

Congratulations on completing your very first interactive application! We covered some of the most critical foundational building blocks of Flutter development in this lesson.

Let’s do a quick recap of the core concepts you mastered today:

  • Stateless vs. Stateful Widgets: You learned that while a StatelessWidget is perfect for static layouts that never alter, a StatefulWidget is required whenever an app needs to remember data and dynamically redraw the screen.
  • The Concept of State: You learned that State is any live data in your app that can change over time while a user interacts with it (like our _counter integer variable).
  • Private Variables (_): You discovered that prefixing a variable or class name with an underscore in Dart makes it private to that specific file, protecting your core logic from unintended edits.
  • The Power of setState(): You mastered the application of setState(). You saw firsthand that changing a background memory variable isn’t enough—we must explicitly tell Flutter to trigger the build() method to repaint the UI with the fresh data.
  • Layout Spacing & Adaptability: You used SizedBox for explicit gaps and configured a defensive Wrap widget layout to automatically prevent ugly pixel overflow errors across different screen sizes.

What’s Next?

Now that you have built a fully functional, interactive application, you have officially taken your first big step into app development!

But don’t stop here—the best way to truly cement these skills is to take what you’ve learned and push it just a little bit further.

Challenges to Try on Your Own

To completely master State and user input, try modifying your project with these three upgrades:

  1. Change the Step Size: Modify the Increment and Decrement button logic so that the counter jumps by 5 or 10 with every single tap instead of just 1.
  2. Add a Maximum Limit: Use an if statement inside your increment logic to block the counter from exceeding a specific maximum limit (for example, stop it from counting past 100).
  3. Change Colors Dynamically: Can you make the text color of the counter turn Red if the number drops back down to 0, and turn Green if the number passes 10?
Scroll to Top