11.1 Stateful Widgets and Forms
# 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}.');
}
}
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.');
}
}
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 thebuild
method. It instantiates a class that extendsState
.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 timesetState()
is called.setState()
: this method is called any time we want to change one of our state variables. It will trigger the running ofbuild
.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++;
})
}
)
)
}
}
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
)
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);
2
# Form and Text Input Widgets
A Form
widget not required but makes it easier to save, reset, or validate all the grouped FormField
s 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
},
),
)
)
}
}
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
}
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()
)
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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');
},
),
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
# 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
# DropdownButton
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`.
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 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