Flutter Apps

11.1 Stateful Widgets and Forms

Module Still Under Development

# Dart Reference

As the code that we write in Flutter becomes more challenging to understand, it can help to have a Dart reference and guide.

# Concept of State

If I asked you what your current state is, it might seem like an unusual question. However, if I split the question into a series of questions like - how do you feel; how old are you; what is your name; who is your favourite professor - then it becomes easier to answer.

All those questions combined into one collection of answers can be called your state.

Apply this to a mobile app, and we can start to think about our application broken into separate visual components (or widgets) and some of those components might have information that is connected to them.

You could have a ListView and that ListView has a state value that is a List of Strings. The ListView could have a builder that creates each of the items inside the ListView. The builder will assign one of the Strings in the List as the text inside each list item. The ListView can be called a Stateful component because the List of Strings is its state value.

If the List is a state value, then we can do things like adding or removing values from the List and use the change in the state value as a trigger to re-build the ListView.

With a StatefulWidget, you get to add any state variables that you want to a State widget. Then have Flutter observe those variables for changes in their value. Each time a variable's value changes, Flutter will re-build the contents of the StatefulWidget.

# Stateless Widget Starting Point

Everything that we have done up until now has been with Stateless Widgets. We build them once and put them on the screen and they never change. Here is a basic stateless widget class to use for comparison with our Stateful ones.

class Fred extends StatelessWidget {
  //we can add instance variables to our class.
  final String name; // once set they can't change
  final int age; //will be referenced as this.age and this.name

  //the constructor for the class that gets called when added
  //as a child widget like this: child: Fred({'fred', 40})
  Fred({this.name, this.age});

  //override the build method that comes from the StatelessWidget class
  
  Widget build(BuildContext context){
    //use `widget` instead of `this`
    return Text('My name is ${widget.name} and my age is ${widget.age}.');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Creating a Stateful Widget

In VSCode we can create the basic template for one by typing something like stfull, which will bring up the code complete list where you can choose Stateful Widget. Once it is added to the screen you will see multiple cursors blinking. Type the name for your Stateful widget before doing anything else.

With our Stateful Widgets we will actually have two classes - one that extends StatefulWidget and a second, that extends State. The inner class will hold the state values that will change over time plus the build method.

class AgingFred extends StatefulWidget{
  final String name; //value created when constructor called
  //name will not change after AgingFred() called
  AgingFred({this.name});

  
  _AgingFredState createState() = _AgingFredState();
  //calls the second class to create the state values
  //and prepare the build method
}

class _AgingFredState extends State<AgingFred>{
  //create your state values here
  int age = 40; //default value
  //age can be updated later and will re-build the widget
  // we would use a setState() method call to change the value of age
  // setState() accepts a function to be called to do the update

  
  Widget build(BuildContext context){
    // name comes from the AgingFred class.
    //count is our state value
    return Text('${widget.name} is $age years old.');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# Stateful Widget Lifecycle

There are a few methods that you should be aware of when working with Stateful Widgets.

  • createState(): this method is used to create all your state variables and wraps the build method. It instantiates a class that extends State.
  • initState(): this method runs once when the widget is created. You can use it to subscribe to objects and data sources that can change and that will change the contents of your widget.
  • build(): this method is called to build the widget tree. It is called on the inital creation of the widget as well as every time setState() is called.
  • setState(): this method is called any time we want to change one of our state variables. It will trigger the running of build.
  • dispose(): this method is called when the widget is removed from the screen. It is a cleanup function, meant to be used to get rid of any async tasks that might still be running.

Let's update the _AgingFredState class from above to add the initState and setState methods.

class _AgingFredState extends State<AgingFred>{
  //create your state values here
  int age = 0; //default value

  
  void initState() {
    super.initState(); // calls the initState function from State
    //called when our widget is created
    //called once and only once
    age = Random().nextInt(100);
    //we can do things to prepare data for our screen here
    //async things like fetching data...
  }

  
  void dispose() {
    super.dispose();
    // runs when we remove this widget from the tree
  }

  
  Widget build(BuildContext context){
    // name comes from the AgingFred class.
    //count is our state value
    return ListTile(
      title: Text('${widget.name} is $age years old.')
      trailing: IconButton(
        icon: Icon(Icons.add),
        onPressed: () {
          setState((){
            age++;
          })
        }
      )

    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# Keys

When you have a list of widgets, like ListTiles inside a ListView and you need to be able to target them through code, then each one should have a key property. This way Flutter can differentiate between them. Think of the keys like HTML id attributes.

If you ever need to get a reference to a single Widget from somewhere else in your code then you can create a global variable that contains a key for a specific widget using the GlobalKey method. Store the return value from the method inside a final variable and then assign the variable to the key property inside your widget that you will need to target.

//inside your class - replace `WidgetType` with actual Widget
final _myKey = GlobalKey<WidgetType>();

//inside the `WidgetType` widget
WidgetType(
  key: _myKey,
  //other properties
)
1
2
3
4
5
6
7
8

# Inherited Widget

The Flutter Inherited Widget is a lot like the React Context Hook object. It lets you store state information in a top level widget that can be accessed by any Widget that is a descendent of the InheritedWidget, regardless of how far down the tree it is.

inherited widgets

You can recognize Inherited widgets by the static of(context) method that they call. Examples:

MediaQuery.of(context);
Theme.of(context);
1
2

# Form and Text Input Widgets

A Form widget not required but makes it easier to save, reset, or validate all the grouped FormFields in the form.

The TextField widget is the most common control.

A TextFormField is a FormField widget wrapped around a TextField to integrate with the Form.

A TextField has an onChanged property with a function that fires while the user is typing and an onSubmitted property that fires when leaving the field.

Add a controller property to watch the value and set initial value. Without a controller, you can use the initialValue property to have default content in the form field when the screen loads.

The decoration property lets you add labels and borders and, by default, draws the line under the field.

Always call TextEditingController.dispose when done to free resources.

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  late TextEditingController _controller;

  
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: TextField(
          keyboardType: TextInputType.text,
          //TextInputType values are:
          //text, multiline, number, phone, datetime, emailAddress, url, visiblePassword, name, streetAddress, none
          maxLength: 80,
          enabled: true, //like HTML disabled: false
          obscureText: false, //for passwords
          readOnly: false,
          style: TextStyle(color: Colors.blue),
          decoration: InputDecoration( labelText: 'First Name' ),
          textInputAction: TextInputAction.next,
          //input action list includes:
          // done, go, join, newline, next, previous, search, send, emergencyCall
          controller: _controller,
          onSubmitted: (String value) {
            print('You have typed $value');
            //you could call setState here
          },
          onChanged: (String value) {
            print('You have typed $value');
            //you could call setState here
          },
        ),
      )
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

Here is the reference for TextInputAction and the reference for the TextInputType for the keyboardType property. Be cautious with the TextInputAction because not all values work on both mobile OS.

If you are expecting users to be able to put emojis in their text then you need to use the strings.characters.length to find the number of characters they typed. Do not use string.length and expect a valid number.

Here is a sample showing a series of TextFormField widgets added to a Form. The Form acts as a wrapper for the three TextFormField widgets

class _HomeState extends State<Home> {
  final GlobalKey<FormState> _formStateKey = GlobalKey<FormState>();

  MyData _data = MyData();

  
  Widget build(BuildContext context){
    return Scaffold(
      body: SafeArea(
        child: Form(
          key: _formStateKey,
          autovalidate: true,
          //runs the validator in each field as user completes each field
          child: Padding(
            padding: EdgeInsets.all(16.0),
            child: Column(
              children: <Widget>[
                TextFormField(
                  decoration: InputDecoration(
                    labelText: 'Gimme info',
                  ),
                  validator: (String? value) {
                    //runs when the form is submitted
                    if(value == null || value.isEmpty){
                      return 'Please enter some text';
                      //this becomes the text for the ErrorText
                      //returning some value means there was an error
                    }
                    return null; //means no error
                  },
                  onSaved: (String value) {
                    //runs after successful call to validator
                    //do what you want with value.
                    //Eg: save it in a data object
                    _data.title = value;
                  }
                ),
                TextFormField(...),
                TextFormField(...),
                TextButton(
                  child: Text('Send'),
                  onPressed: () {
                    if( _formStateKey.currentState!.validate() ){
                      //validate returns true if form fields are good
                      _formStateKey.currentState.save();
                      //triggers the onSave functions in the TextFormFields
                    }else{
                      //form data not valid
                    }
                  }
                )
              ]
            )
          )
        )
      )
    )
  }
}

class MyData{
  //this is like a plain Object in JS to hold properties
  String title;
  int quantity;
  double price;
  String? url; //nullable - can be left null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

Here are some useful Cookbook recipes from the Flutter site:

# InputDecoration

The InputDecoration class is used to format the appearance of the TextField widget beyond TextStyle properties. It covers messages to the user, borders and things like icon buttons within the text control.

//example with TextField and InputDecoration
TextField(
  keyboardType: TextInputType.text,
  initialValue: '',
  decoration: InputDecoration(
    labelText: 'First Name',
    icon: Icon(Icons.send),
    prefix: Text('Dr. '),
    prefixIcon: Icon(Icons.medical_services),
    suffix: Text('MD'),
    hintText: 'Please provide your first name. No Emojis.',
    helperText: 'Must be between 2 and 30 characters.',
    counterText: '0 characters',
    errorText: 'Wrong again!',
    errorStyle: TextStyle(color: Colors.red),
    errorBorder: OutlineInputBorder()
    border: UnderlineInputBorder()
  )
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

InputDecoration reference

# Text Input and State

Handling input from the user through a TextField can be done with a basic onChange listener but is most usually done with a controller.

//the basic version
TextField(
  keyboardType: TextInputType.emailAddress,
  decoration: InputDecoration(
    labelText: 'First Name',

  ),
  onChanged: (text) {
    print('First name text field: $text');
  },
),
1
2
3
4
5
6
7
8
9
10
11

As you saw in the example above with the validator and onSave properties of the TextFormField we can create a custom class which becomes just a data storage object to group and hold all the form values.

That object can be used as a state variable too. You can connect calls to setState() through the various onSave, onSubmitted and onChanged property functions.

This video shows how to work with all the different TextFields, how to use Edit controllers, and some styling too.

Deep Dive into TextField

This video shows how to use the built-in DateTime picker in your form.

DateTimePicker

showDatePicker Reference

# Full Form Solution

This is a repo that includes a Form and TextFormFields and includes examples of doing form validation and submission. We will be reviewing this in class this week.

# ToggleButtons

ToggleButton widgets let you set up a list of 2 or more things for the user to select from. Think of a row of icons that could be selected. Or an on/off toggle switch.

ToggleButtons

ToggleButton reference

The DropdownButton also lets the user select from a list of items. However, this is much more like an HTML <select>.

DropdownButton<String>(
  value: dropdownValue,
  icon: const Icon(Icons.arrow_downward),
  elevation: 16,
  style: const TextStyle(color: Colors.deepPurple),
  underline: Container(
    height: 2,
    color: Colors.deepPurpleAccent,
  ),
  onChanged: (String? newValue) {
    setState(() {
      dropdownValue = newValue!;
    });
  },
  items: <String>['One', 'Two', 'Free', 'Four']
      .map<DropdownMenuItem<String>>((String value) {
    return DropdownMenuItem<String>(
      value: value,
      child: Text(value),
    );
  }).toList(),
  //for items we take a List of strings and then map that to
  //a collection of DropdownMenuItem widgets with value and child properties.
  //then at the end, convert those back to a List that will be assigned to `items`.
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

DropdownButton reference

# What to do this week

TODO Things to do before next week

  • Read and watch all the content from modules 11.1 and 11.2
Last Updated: 11/13/2023, 9:45:26 AM