Flutter Stateful and stateless widgets

flutter, widget, foozaweb, google, software, development

A widget is either stateful or stateless. If a widget can change—when a user interacts with it, for example—it’s stateful.

stateless widget never changes. IconIconButton, and Text are examples of stateless widgets. Stateless widgets subclass StatelessWidget.

stateful widget is dynamic: for example, it can change its appearance in response to events triggered by user interactions or when it receives data. CheckboxRadioSliderInkWellForm, and TextField are examples of stateful widgets. Stateful widgets subclass StatefulWidget.

A widget’s state is stored in a State object, separating the widget’s state from its appearance. The state consists of values that can change, like a slider’s current value or whether a checkbox is checked. When the widget’s state changes, the state object calls setState(), telling the framework to redraw the widget.

Creating a stateful widget

What's the point?

  • A stateful widget is implemented by two classes: a subclass of StatefulWidget and a subclass of State.
  • The state class contains the widget’s mutable state and the widget’s build() method.
  • When the widget’s state changes, the state object calls setState(), telling the framework to redraw the widget.

In this section, you’ll create a custom stateful widget. You’ll replace two stateless widgets—the solid red star and the numeric count next to it—with a single custom stateful widget that manages a row with two children widgets: an IconButton and Text.

Implementing a custom stateful widget requires creating two classes:

This section shows you how to build a stateful widget, called FavoriteWidget, for the lakes app. After setting up, your first step is choosing how the state is managed for FavoriteWidget.

Step 0: Get ready

If you’ve already built the app in the building layouts tutorial (step 6), skip to the next section.

  1. Make sure you’ve set up your environment.
  2. Create a basic “Hello World” Flutter app.
  3. Replace the lib/main.dart file with main.dart.
  4. Replace the pubspec.yaml file with pubspec.yaml.
  5. Create an images directory in your project, and add lake.jpg.

Once you have a connected and enabled device, or you’ve launched the iOS simulator (part of the Flutter install) or the Android emulator (part of the Android Studio install), you are good to go!

Step 1: Decide which object manages the widget’s state

A widget’s state can be managed in several ways, but in our example, the widget itself, FavoriteWidget, will manage its own state. In this example, toggling the star is an isolated action that doesn’t affect the parent widget or the rest of the UI, so the widget can handle its state internally.

Learn more about the separation of widget and state, and how the state might be managed, in Managing state.

Step 2: Subclass StatefulWidget

The FavoriteWidget class manages its own state, so it overrides createState() to create a State object. The framework calls createState() when it wants to build the widget. In this example, createState() returns an instance of _FavoriteWidgetState, which you’ll implement in the next step.

 

lib/main.dart (FavoriteWidget)


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

  @override
  State createState() => _FavoriteWidgetState();
}

 

Note: Members or classes that start with an underscore (_) are private. For more information, see Libraries and visibility, a section in the Dart language tour.

Step 3: Subclass State

The _FavoriteWidgetState class stores the mutable data that can change over the lifetime of the widget. When the app first launches, the UI displays a solid red star, indicating that the lake has “favorite” status, along with 41 likes. These values are stored in the _isFavorited and _favoriteCount fields:

 


 

lib/main.dart (_FavoriteWidgetState fields)


class _FavoriteWidgetState extends State {
  bool _isFavorited = true;
  int _favoriteCount = 41;

  // ···
}

 

The class also defines a build() method, which creates a row containing a red IconButton, and Text. You use IconButton (instead of Icon) because it has an onPressed property that defines the callback function (_toggleFavorite) for handling a tap. You’ll define the callback function next.


 

lib/main.dart (_FavoriteWidgetState build)

class _FavoriteWidgetState extends State {
  // ···
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          padding: const EdgeInsets.all(0),
          child: IconButton(
            padding: const EdgeInsets.all(0),
            alignment: Alignment.centerRight,
            icon: (_isFavorited
                ? const Icon(Icons.star)
                : const Icon(Icons.star_border)),
            color: Colors.red[500],
            onPressed: _toggleFavorite,
          ),
        ),
        SizedBox(
          width: 18,
          child: SizedBox(
            child: Text('$_favoriteCount'),
          ),
        ),
      ],
    );
  }
}


 

Tip: Placing the Text in a SizedBox and setting its width prevents a discernible “jump” when the text changes between the values of 40 and 41 — a jump would otherwise occur because those values have different widths.

The _toggleFavorite() method, which is called when the IconButton is pressed, calls setState(). Calling setState() is critical, because this tells the framework that the widget’s state has changed and that the widget should be redrawn. The function argument to setState() toggles the UI between these two states:


 

void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}


 

Step 4: Plug the stateful widget into the widget tree

Add your custom stateful widget to the widget tree in the app’s build() method. First, locate the code that creates the Icon and Text, and delete it. In the same location, create the stateful widget:


 

class MyApp extends StatelessWidget {            
                                   const MyApp({super.key});            
        @@ -40,11 +35,7 @@    
                                               ],            
                                             ),            
                                           ),            
                    -                      Icon(            
                    -                        Icons.star,            
                    -                        color: Colors.red[500],            
                    -                      ),            
                    -                      const Text('41'),            
                    +                      const FavoriteWidget(),            
                                         ],            
                                       ),            
                                     );


 

That’s it! When you hot reload the app, the star icon should now respond to taps.

Problems?

If you can’t get your code to run, look in your IDE for possible errors. Debugging Flutter apps might help. If you still can’t find the problem, check your code against the interactive lakes example on GitHub.

If you still have questions, refer to any one of the developer community channels.


The rest of this page covers several ways a widget’s state can be managed, and lists other available interactive widgets.

Managing state

What's the point?

  • There are different approaches for managing state.
  • You, as the widget designer, choose which approach to use.
  • If in doubt, start by managing state in the parent widget.

Who manages the stateful widget’s state? The widget itself? The parent widget? Both? Another object? The answer is… it depends. There are several valid ways to make your widget interactive. You, as the widget designer, make the decision based on how you expect your widget to be used. Here are the most common ways to manage state:

How do you decide which approach to use? The following principles should help you decide:

If in doubt, start by managing the state in the parent widget.

We’ll give examples of the different ways of managing state by creating three simple examples: TapboxA, TapboxB, and TapboxC. The examples all work similarly—each creates a container that, when tapped, toggles between a green or grey box. The _active boolean determines the color: green for active or grey for inactive.

Active stateInactive state

These examples use GestureDetector to capture activity on the Container.

The widget manages its own state

Sometimes it makes the most sense for the widget to manage its state internally. For example, ListView automatically scrolls when its content exceeds the render box. Most developers using ListView don’t want to manage ListView’s scrolling behavior, so ListView itself manages its scroll offset.

The _TapboxAState class:


 

import 'package:flutter/material.dart';

// TapboxA manages its own state.

//------------------------- TapboxA ----------------------------------

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

  @override
  State createState() => _TapboxAState();
}

class _TapboxAState extends State {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

//------------------------- MyApp ----------------------------------

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Demo'),
        ),
        body: const Center(
          child: TapboxA(),
        ),
      ),
    );
  }
}


 

 

The parent widget manages the widget’s state

Often it makes the most sense for the parent widget to manage the state and tell its child widget when to update. For example, IconButton allows you to treat an icon as a tappable button. IconButton is a stateless widget because we decided that the parent widget needs to know whether the button has been tapped, so it can take appropriate action.

In the following example, TapboxB exports its state to its parent through a callback. Because TapboxB doesn’t manage any state, it subclasses StatelessWidget.

The ParentWidgetState class:

The TapboxB class:


 

import 'package:flutter/material.dart';

// ParentWidget manages the state for TapboxB.

//------------------------ ParentWidget --------------------------------

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

  @override
  State createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  const TapboxB({
    super.key,
    this.active = false,
    required this.onChanged,
  });

  final bool active;
  final ValueChanged onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
      ),
    );
  }
}


 

A mix-and-match approach

For some widgets, a mix-and-match approach makes the most sense. In this scenario, the stateful widget manages some of the state, and the parent widget manages other aspects of the state.

In the TapboxC example, on tap down, a dark green border appears around the box. On tap up, the border disappears and the box’s color changes. TapboxC exports its _active state to its parent but manages its _highlight state internally. This example has two State objects, _ParentWidgetState and _TapboxCState.

The _ParentWidgetState object:

The _TapboxCState object:


 

import 'package:flutter/material.dart';

//---------------------------- ParentWidget ----------------------------

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

  @override
  State createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {
  const TapboxC({
    super.key,
    this.active = false,
    required this.onChanged,
  });

  final bool active;
  final ValueChanged onChanged;

  @override
  State createState() => _TapboxCState();
}

class _TapboxCState extends State {
  bool _highlight = false;

  void _handleTapDown(TapDownDetails details) {
    setState(() {
      _highlight = true;
    });
  }

  void _handleTapUp(TapUpDetails details) {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTapCancel() {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTap() {
    widget.onChanged(!widget.active);
  }

  @override
  Widget build(BuildContext context) {
    // This example adds a green border on tap down.
    // On tap up, the square changes to the opposite state.
    return GestureDetector(
      onTapDown: _handleTapDown, // Handle the tap events in the order that
      onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(
                  color: Colors.teal[700]!,
                  width: 10.0,
                )
              : null,
        ),
        child: Center(
          child: Text(widget.active ? 'Active' : 'Inactive',
              style: const TextStyle(fontSize: 32.0, color: Colors.white)),
        ),
      ),
    );
  }
}


 

An alternate implementation might have exported the highlight state to the parent while keeping the active state internal, but if you asked someone to use that tap box, they’d probably complain that it doesn’t make much sense. The developer cares whether the box is active. The developer probably doesn’t care how the highlighting is managed, and prefers that the tap box handles those details.


Other interactive widgets

Flutter offers a variety of buttons and similar interactive widgets. Most of these widgets implement the Material Design guidelines, which define a set of components with an opinionated UI.

If you prefer, you can use GestureDetector to build interactivity into any custom widget. You can find examples of GestureDetector in Managing state. Learn more about the GestureDetector in Handle taps, a recipe in the Flutter cookbook.

 Tip: Flutter also provides a set of iOS-style widgets called Cupertino.

When you need interactivity, it’s easiest to use one of the prefabricated widgets. Here’s a partial list:

Standard widgets

Material Components

Resources

The following resources might help when adding interactivity to your app.

Gestures, a section in the Flutter cookbook.

Handling gestures, a section in Introduction to widgets

How to create a button and make it respond to input.

Gestures in Flutter

A description of Flutter’s gesture mechanism.

Flutter API documentation

Reference documentation for all of the Flutter libraries.

Flutter Gallery running apprepo

Demo app showcasing many Material components and other Flutter features.

Flutter’s Layered Design (video)

This video includes information about state and stateless widgets. Presented by Google engineer, Ian Hickson

Interested in hiring us for your project?

To start an initial chat, just drop an email at mail@foozawebtech.com or use the form on the contact page.