Google Flutter - Stateful Widgets

Google Flutter, Google Flutter Stateful Widgets -

Google Flutter - Stateful Widgets

Introduction

In the previous article we introduced stateless widgets and how they can be used. Now it is time to introduce the more advanced stateful widgets. Stateful widgets are not used as often as stateless widgets but they tend to be more important as they are what makes the ui responsive to user input.

Stateful widgets are useful when the part of the user interface you are describing can change dynamically. User interfaces need to respond to a variety of things:

  • The user doing something in the user interface.
  • Receiving data from another computer.
  • Time passing.

Examples of Stateful Widgets

 

  • Text box that updates when you type into it.
  • A button that shows a shadow when you press it.
  • A clock widget that shows the time.

Stateful Widgets Are Made Up Of Two "Things", Not One

 

  • The Widget.
    • Creates the State Object.
  • The State Object (created by the widget and associated with the widget).
    • Holds state.
    • Has event handler code.
    • Has build method to redraw ui.

How Does Their UI Update When State Changes?

 

  • The Stateful Widget holds its state using instance variables in a State Object.
  • When an Event happens, the code in the State object (added by you) changes the value of an instance variable (added by you), invoking the 'setState' method to do so. More on that soon.
  • The Stateful Widget knows that it has to rebuild the UI because the 'setState' method was invoked. So it invokes the 'build' method (added by you) to build the Widget Data it needs to rebuild the ui.
  • The Flutter runtime takes the Widget Data returned from the 'build' method and uses it to redraw the ui.

Minimum Code

Here is the minimum code you need for a Stateful Widget:



        class EmptyWidget extends StatefulWidget {
            EmptyWidget({Key key}) : super(key: key);
          
            @override
            _EmptyWidgetState createState() => _EmptyWidgetState();
        }
          
        class _EmptyWidgetState extends State {
            @override
            Widget build(BuildContext context) {
              return ;
            }
        }          
    

The Stateful Widget is this part.


        class EmptyWidget extends StatefulWidget {
            EmptyWidget({Key key}) : super(key: key);
          
            @override
            _EmptyWidgetState createState() => _EmptyWidgetState();
        }
          
    

The State Object is this part.


        class _EmptyWidgetState extends State {
            @override
            Widget build(BuildContext context) {
              return ;
            }
        }          
    

Now Lets Look At the Stateful Widget

 

  • Dart class that extends StatefulWidget.
  • This is a class that is used to create the State object, class #2 in its ‘createState’ method.
  • An instance of this class is shorter-lived than that for the State Object.
  • The data in this class cannot change (immutable).
    • It is final and passed in through the constructor, same as for a StatelessWidget.
    • This class is thrown away and replaced when the data needs to change, and a new Widget is constructed.

Now Lets Look At the State Object

 

  • Dart class that extends State.
  • This is the class that does most of the work.
    • It holds the data that can change (mutable).
    • It builds the UI using the ‘build’ method.
    • It can respond to events, like the user clicking on a button.
  • An instance of this class is longer-lived than that for the StatefulWidget, class #1.
  • The data in this class can change.
    • Change the data within a lambda within the ‘setState’ method and this will ensure the UI is rebuilt.
    • The StatefulWidget class #1 can be thrown away and replaced and this state is then attached to the replacement.
  • Code in this class can refer to Stateful Widget by using the ‘widget’ variable.
  • Sometimes this class uses an underscore prefix in its name, instance variables or methods.
    • In Dart the underscore prefix specifies something as private and cannot be used outside the dart file in which it is declared.
    • This protects this class, its instance variable or methods being used in another part of the project (not a good idea).

Stateful Widget Creation In More Depth

When you create a Stateful Widget the following happens.

  1. The instance of Stateful Widget (the class that extends StatefulWidget) is constructed.
  2. The lifecycle method ‘createState’ of class #1 (the class that extends StatefulWidget) is invoked by Flutter to create the State Object (the class that extends State).
  3. The instance of the State Object (the class that extends State) is constructed.
  4. The method ‘build’ of the State Object class (created in 3) is invoked to build the UI.

Stateful Widget Rendering In More Depth

 

  1. Stateful Widgets generate their UI in their ‘build’ method, the result of which is rendered by Flutter.
    • That ‘build’ method resides in the State Object, the class that extends State.
  2. They can build their UI using values from their member variables, other sources.
  3. They can force themselves to re-render.
  4. When the Stateful Widget method ‘setState’ is called in the State class, this invokes regeneration of the UI because it causes Flutter to invoke the ‘build’ method.
    • If you look at the default Flutter application, you will see this method to increment the counter.
    • Note the following:
      • How it updates the instance variable ‘_counter’ in a lambda inside the ‘setState’ method.
      • How this ensures that the UI will be rebuilt with the new counter value.

Example

This is an app that lets you view a list of cars and tap on one to select it.

The code for this example is here: https://github.com/markclow/flutter_book_examples/tree/master/stateful_widget_flowers.

Step 1 – Create Default Flutter App

Follow the instructions here.

Step 2 – Replace Application Code

Replace the contents of file ‘main.dart’ in folder ‘lib’ with the following:


            import 'package:flutter/material.dart';

            void main() => runApp(new MyApp());
            
            class MyApp extends StatelessWidget {
              @override
              Widget build(BuildContext context) {
                return new MaterialApp(
                  title: 'Flutter Demo',
                  theme: new ThemeData(
                    primarySwatch: Colors.blue,
                  ),
                  home: new MyHomePage(),
                );
              }
            }
            
            class Car {
              String _make;
              String _model;
              String _imageSrc;
            
              Car(this._make, this._model, this._imageSrc);
            
              operator ==(other) =>
                  (other is Car) && (_make == other._make) && (_model == other._model);
            }
            
            class MyHomePage extends StatefulWidget {
              @override
              MyHomePageState createState() => MyHomePageState("Cars");
            }
            
            class MyHomePageState extends State {
              String _title;
              List _cars;
              Car _selectedCar;
            
              MyHomePageState(this._title) {
                _cars = [
                  Car(
                    "Bmw",
                    "M3",
                    "https://media.ed.edmunds-media.com/bmw/m3/2018/oem/2018_bmw_m3_sedan_base_fq_oem_4_150.jpg",
                  ),
                  Car(
                    "Nissan",
                    "GTR",
                    "https://media.ed.edmunds-media.com/nissan/gt-r/2018/oem/2018_nissan_gt-r_coupe_nismo_fq_oem_1_150.jpg",
                  ),
                  Car(
                    "Nissan",
                    "Sentra",`
                    "https://media.ed.edmunds-media.com/nissan/sentra/2017/oem/2017_nissan_sentra_sedan_sr-turbo_fq_oem_4_150.jpg",
                  )
                ];
              }
            
              void _selectionHandler(Car selectedCar) {
                setState(() {
                  _title = 'Selected ${selectedCar._make} ${selectedCar._model}';
                  _selectedCar = selectedCar;
                });
              }
            
              @override
              Widget build(BuildContext context) {
                List carWidgets = _cars.map((Car car) {
                  return CarWidget(car, car == _selectedCar, _selectionHandler);
                }).toList();
                return new Scaffold(
                    appBar: new AppBar(
                      title: new Text(_title),
                    ),
                    body: new ListView(children: carWidgets));
              }
            }
            
            class CarWidget extends StatelessWidget {
              CarWidget(this._car, this._isSelected, this._parentSelectionHandler)
                  : super();
            
              final Car _car;
              final bool _isSelected;
              final ValueChanged _parentSelectionHandler;
            
              void _handleTap() {
                _parentSelectionHandler(_car);
              }
            
              @override
              Widget build(BuildContext context) {
                return Padding(
                    padding: EdgeInsets.all(20.0),
                    child: GestureDetector(
                        onTap: _handleTap,
                        child: Container(
                            decoration: BoxDecoration(
                                color: _isSelected ? Colors.lightBlueAccent : Colors.white,
                                border: Border.all()),
                            padding: EdgeInsets.all(20.0),
                            child: Center(
                                child: Column(children: [
                              Text('${_car._make} ${_car._model}',
                                  style: TextStyle(fontSize: 24.0)),
                              Padding(
                                  padding: EdgeInsets.only(top: 20.0),
                                  child: Image.network(_car._imageSrc))
                            ])))));
              }
            }            
        

Now Lets Examine the Code from the Top Down

  • We create a data object Car.
    • This is a plain Dart object that stores the data for a car.
    • Notice that it overloads the equality operator so that Dart can tell if that cars are the same if they have matching make & model values.
  • We create the Home Page Stateful Widget.
    • Note how it creates the Home Page State Object.
  • We create the Home Page State Object.
    • This is a Stateful Widget because it changes the UI when the user taps a Car.
    • It has 3 instance variables: _title, _cars and _selectedCar.
    • It sets up the list of cars (variable _cars) in its constructor.
    • Handles Tap Event.
      • It has a method '_selectionHandler' that handles the event of a car being tapped on and thus selected.
      • This method has an argument that passes in the data object for the selected car.
      • Note that this invokes the 'setState' Flutter method, passing using a lambda to that method as an argument.
      • The lambda sets the value of the '_title' instance variable to indicate the selected car. The lambda also sets the value of the '_selectedCar' instance variable.
    • Builds UI.
      • It has a method 'build' that builds the UI.
      • It builds a List (a Flutter component for vertical lists with a scrollbar) of Car Widgets.
      • It builds the List by transforming the data (using a map function) in the '_cars' instance variable into a the List of Car Widgets.
      • Notice how the map function creates each CarWidget passing in three arguments: the Car data object, a true or false indicating if the car is the selected one or not and the '_selectionHandler' method.
      • It returns a Scaffold Widget (a Flutter Widget that is used to build an app) containing the title from the instance variable and the List of Car Widgets in the main area.
  • We create the CarWidget.
    • This is a Stateless Widget because all it does is display a car and captures the Tap Event.
    • It doesnt handle the UI response to the Tap, it just invokes the handler method that was passed to it by the Home Page Stateful Widget.
    • Note how we pass all the information required to display a Car to the Car Widget through its constructor, which sets instance variables inside it, which are used later by the 'build' method to build the ui for the widget.
    • Builds UI Containing GestureDetector to Detect Tap
      • Note how the 'build' method wraps the UI in a GestureDetector.
      • This GestureDetector is used to detect the tap. When the tap occurs, the '_handleTap' method is invoked.
      • This method then invokes the method '_selectionHandler' in the parent Home Page Stateful Widget, which was stored as instance variable '_parentSelectionHandler'.