© Rap Payne 2019
R. PayneBeginning App Development with Flutterhttps://doi.org/10.1007/978-1-4842-5181-2_4

4. Value Widgets

Rap Payne1 
(1)
Dallas, TX, USA
 

We learned in the last chapter that everything is a widget. Everything you create is a widget and everything that Flutter provides us is a widget. Sure, there are exceptions to that, but it never hurts to think of it this way, especially as you’re getting started in Flutter. In this chapter we’re going to drill down into the most fundamental group of widgets that Flutter provides us – the ones that hold a value. We’ll talk about the Text widget, the Icon widget, and the Image widget, all of which display exactly what their names imply. Then we’ll dive into the input widgets – ones designed to get input from the user.

The Text widget

If you want to display a string to the screen, the Text widget is what you’ll need.
Text('Hello world'),

Tip

If your Text is a literal, put the word const in front of it and the widget will be created at compile time instead of runtime. Your apk/ipa file will be slightly larger but they’ll run faster on the device. Well worth it.

You have control over the Text’s size, font, weight, color, and more with its style property. But we’ll cover that in Chapter 8, “Styling Your Widgets.”

The Icon widget

Flutter comes with a rich set of built-in icons (Figure 4-1), from cameras to people to cards to vehicles to arrows to batteries to Android/iOS devices. A full list can be found here: https://api.flutter.dev/flutter/material/Icons-class.html.
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig1_HTML.jpg
Figure 4-1

An assortment of Flutter’s built-in widgets in random colors

To place an icon, you use the Icon widget. No surprise there. You use the Icons class to specify which one. This class has hundreds of static values like Icons.phone_android and Icons.phone_iphone and Icons.cake. Each points to a different icon like the ones pictured previously. Here’s how you’d put a big red birthday cake (Figure 4-2) on your app:
Icon(
  Icons.cake,
  color: Colors.red,
  size: 200,
)
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig2_HTML.jpg
Figure 4-2

The red cake icon

The Image widget

Displaying images in Flutter is a bit more complex than Text or Icons. It involves a few things:
  1. 1.

    Getting the image source – This could be an image embedded in the app itself or fetched live from the Internet. If the image will never change through the life of your app like a logo or decorations, it should be an embedded image.

     
  2. 2.

    Sizing it – Scaling it up or down to the right size and shape.

     

Embedded images

Embedded images are much faster but will increase your app’s install size. To embed the image, put the image file in your project folder, probably in a subfolder called images just to keep things straight. Something like assets/images will do nicely.

Then edit pubspec.yaml. Add this to it:
flutter:
  assets:
   - assets/images/photo1.png
   - assets/images/photo2.jpg

Save the file and run “flutter pub get” from the command line to have your project process the file.

Tip

The pubspec.yaml file holds all kinds of great information about your project. It holds project metadata like the name, description, repository location, and version number. It lists library dependencies and fonts. It is the go-to location for other developers new to your project. For any of you JavaScript developers, it is the package.json file of your Dart project.

Then you’ll put the image in your custom widget by calling the asset() constructor like this:
Image.asset('assets/images/photo1.jpg',),

Network images

Network images are much more like what web developers might be accustomed to. It is simply fetching an image over the Internet via HTTP. You’ll use the network constructor and pass in a URL as a string.
Image.network(imageUrl),

As you’d expect, these are slower than embedded images because there’s a delay while the request is being sent to a server over the Internet and the image is being downloaded by your device. The advantage is that these images are live; any image can be loaded dynamically by simply changing the image URL.

Sizing an image

Images are nearly always put in a container. Not that this is a requirement, it’s just that I can’t imagine a real-world use case where it won’t be inside another widget. The container has a say in the size that an image is drawn. It would be an amazing coincidence if the Image’s natural size fit its container’s size perfectly. Instead, Flutter’s layout engine will shrink the image to fit its container, but not grow it. This fit is called BoxFit.scaleDown , and it makes sense for the default behavior. But what other options are available and how do we decide which to use? Table 4-1 provides your BoxFit options.
Table 4-1

BoxFit options

fill

Stretch it so that both the width and the height fit exactly. Distort the image

../images/482531_1_En_4_Chapter/482531_1_En_4_Figa_HTML.jpg

cover

Shrink or grow until the space is filled. The top/bottom or sides will be clipped

../images/482531_1_En_4_Chapter/482531_1_En_4_Figb_HTML.jpg

fitHeight

Make the height fit exactly. Clip the width or add extra space as needed

../images/482531_1_En_4_Chapter/482531_1_En_4_Figc_HTML.jpg

fitWidth

Make the width fit. Clip the height or add extra space as needed

../images/482531_1_En_4_Chapter/482531_1_En_4_Figd_HTML.jpg

contain

Shrink until both the height and the width fit. There will be extra space on the top/bottom or sides

../images/482531_1_En_4_Chapter/482531_1_En_4_Fige_HTML.jpg

Photo courtesy of Eye for Ebony on Unsplash

So those are your options, but how do you choose? Figure 4-3 may help you decide which fit to use in different situations.
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig3_HTML.png
Figure 4-3

How to decide an image’s fit

To specify the fit, you’ll set the fit property.
Image.asset('assets/images/woman.jpg',
  fit: BoxFit.contain,),

Input widgets

Many of us came from a web background where from the very beginning there were HTML <form>s with <input>s and <select>s. All of these exist to enable the user to get data into web apps, an activity we can’t live without in mobile apps as well. Flutter provides widgets for entering data like we have in the Web, but they don’t work the same way. They take much more work to create and use. Sorry about that. But they are also safer and give us much more control.

Part of the complication is that these widgets don’t maintain their own state; you have to do it manually.

Another part of the complication is that input widgets are unaware of each other. In other words, they don’t play well together until you group them with a Form widget. We eventually need to focus on the Form widget. But before we do, let’s study how to create text fields, checkboxes, radio buttons, sliders, and dropdowns.

Caution

Input widgets are really tough to work with unless they are used within a StatefulWidget because by nature, they change state. Remember that we mentioned StatefulWidgets briefly in the last chapter and we’re going to talk about them in depth in Chapter 9, “Managing State.” But until then, please just take our word for it and put them in a stateful widget for now.

Text fields

If all you have is a single textbox, you probably want a TextField widget. Here’s a simple example of the TextField widget with a Text label above it:
const Text('Search terms'),
TextField(
  onChanged: (String val) => _searchTerm = val,
),

That onChanged property is an event handler that fires after every keystroke. It receives a single value – a String. This is the value that the user is typing. In the preceding example, we’re setting a local variable called _searchTerm to whatever the user types.

To provide an initial value with a TextField, you need the unnecessarily complex TextInputController:
TextEditingController _controller =
    TextEditingController(text: "Initial value here");
Then tell your TextField about the controller
const Text('Search terms'),
TextField(
  controller: _controller,
  onChanged: (String val) => _searchTerm = val,
),

You can also use that _controller.text property to retrieve the value that the user is typing into the box.

Did you notice the Text(‘Search terms’)? That is our lame attempt at putting a label above the TextField. There’s a much, much better way. Check this out ...

Making your TextField fancy

There’s a ton of options to make your TextField more useful – not infinite options, but lots. And they’re all available through the InputDecoration widget (Figure 4-4):
return TextField(
  controller: _emailController,
  decoration: InputDecoration(
    labelText: 'Email',
    hintText: '[email protected]',
    icon: Icon(Icons.contact_mail),
  ),
),
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig4_HTML.jpg
Figure 4-4

A TextField with an InputDecoration

Table 4-2 presents some more InputDecoration options.
Table 4-2

Input decoration options

Property

Description

labelText

Appears above the TextField. Tells the user what this TextField is for

hintText

Light ghost text inside the TextField. Disappears as the user begins typing

errorText

Error message that appears below the TextField. Usually in red. It is set automatically by validation (covered later), but you can set it manually if you need to

prefixText

Text in the TextField to the left of the stuff the user types in

suffixText

Same as prefixText but to the far right

icon

Draws an icon to the left of the entire TextField

prefixIcon

Draws one inside the TextField to the left

suffixIcon

Same as  prefixIcon but to the far right

Tip

To make it a password box (Figure 4-5), set obscureText property to true. As the user types, each character appears for a second and is replaced by a dot.

return TextField(
  obscureText: true,
  decoration: InputDecoration(
    labelText: 'Password',
  ),);
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig5_HTML.jpg
Figure 4-5

A password box with obscureText

Want a special soft keyboard? No problem. Just use the keyboardType property. Results are shown in Figures 4-6 through 4-9.
return TextField(
  keyboardType: TextInputType.number,
);
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig6_HTML.jpg
Figure 4-6

TextInputType.datetime

../images/482531_1_En_4_Chapter/482531_1_En_4_Fig7_HTML.jpg
Figure 4-7

TextInputType.email. Note the @ sign

../images/482531_1_En_4_Chapter/482531_1_En_4_Fig8_HTML.jpg
Figure 4-8

TextInputType.number

../images/482531_1_En_4_Chapter/482531_1_En_4_Fig9_HTML.jpg
Figure 4-9

TextInputType.phone

Tip

If you want to limit the type of text that is allowed to be entered, you can do so with the TextInput’s inputFormatters property. It’s actually an array so you can combine one or more of ...

  • BlacklistingTextInputFormatter – Forbids certain characters from being entered. They just don’t appear when the user types.

  • WhitelistingTextInputFormatter – Allows only these characters to be entered. Anything outside this list doesn’t appear.

  • LengthLimitingTextInputFormatter – Can’t type more than X characters.

Those first two will allow you to use regular expressions to specify patterns that you want (white list) or don’t want (black list). Here’s an example:

return TextField(

  inputFormatters: [

    WhitelistingTextInputFormatter(RegExp('[0-9 -]')),

    LengthLimitingTextInputFormatter(16)

  ],

  decoration: InputDecoration(

    labelText: 'Credit Card',

  ),

);

In the WhitelistingTextInputFormatter, we’re only allowing numbers 0–9, a space, or a dash. Then the LengthLimitingTextInputFormatter is keeping to a max of 16 characters.

Checkboxes

Flutter checkboxes (Figure 4-10) have a boolean value property and an onChanged method which fires after every change. Like all of the other input widgets, the onChanged method receives the value that the user set. Therefore, in the case of Checkboxes, that value is a bool.
Checkbox(
  value: true,
  onChanged: (bool val) => print(val)),
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig10_HTML.jpg
Figure 4-10

A Flutter Checkbox widget

Tip

A Flutter Switch (Figure 4-11) serves the same purpose as a Checkbox – it is on or off. So the Switch widget has the same options and works in the same way. It just looks different.

../images/482531_1_En_4_Chapter/482531_1_En_4_Fig11_HTML.jpg
Figure 4-11

A Flutter Switch widget

Radio buttons

Of course the magic in a radio button is that if you select one, the others in the same group are deselected. So obviously we need to group them somehow. In Flutter, Radio widgets are grouped when you set the groupValue property to the same local variable. This variable holds the value of the one Radio that is currently turned on.

Each Radio also has its own value property, the value associated with that particular widget whether it is selected or not. In the onChanged method, you’ll set the groupValue variable to the radio’s value:
SearchType _searchType;
//Other code goes here
Radio<SearchType>(
    groupValue: _searchType,
    value: SearchType.anywhere,
    onChanged: (SearchType val) => _searchType = val),
const Text('Search anywhere'),
Radio<SearchType>(
    groupValue: _searchType,
    value: SearchType.text,
    onChanged: (SearchType val) => _searchType = val),
const Text('Search page text'),
Radio<SearchType>(
    groupValue: _searchType,
    value: SearchType.title,
    onChanged: (SearchType val) => _searchType = val),
const Text('Search page title'),
This simplified code would create something like Figure 4-12.
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig12_HTML.jpg
Figure 4-12

Flutter Radio widgets

Sliders

A slider is a handy affordance when you want your user to pick a numeric value between an upper and lower limit (Figure 4-13).
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig13_HTML.jpg
Figure 4-13

A slider with the value of 25

To get one in Flutter, you’ll use the Slider widget which requires an onChanged event and a value property, a double. It also has a min which defaults to 0.0 and a max which defaults to 1.0. A range of zero to one is rarely useful, so you’ll usually change that. It also has a label property which is an indicator telling the user what value they’re choosing.
Slider(
  label: _value.toString(),
  min: 0, max: 100,
  divisions: 100,
  value: _value,
  onChanged: (double val) => _value = val,
),

Dropdowns

Dropdowns are great for picking one of a small number of things, like in an enumeration. Let’s say we have an enum like this:
enum SearchType { web, image, news, shopping }
Where obviously we’re defining a “SearchType” as either “web,” “image,” “news,” or “shopping.” If we wanted our user to choose from one of those, we might present them with a DropdownButton widget that might look like Figure 4-14 to start with.
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig14_HTML.jpg
Figure 4-14

DropdownButton with nothing chosen

Then, when they tap the dropdown, it looks like Figure 4-15.
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig15_HTML.jpg
Figure 4-15

DropdownButton expanded to show the choices

And when they tap one of the options, it is chosen (Figure 4-16).
../images/482531_1_En_4_Chapter/482531_1_En_4_Fig16_HTML.jpg
Figure 4-16

DropdownButton with an option selected

To create that DropdownButton, our Flutter code might look like this:
SearchType _searchType = SearchType.web;
//Other code goes here
DropdownButton<SearchType>(
  value: _searchType,
  items: const <DropdownMenuItem<SearchType>>[
    DropdownMenuItem<SearchType>(
      child:Text('Web'),
      value: SearchType.web,
    ),
    DropdownMenuItem<SearchType>(
      child:Text('Image'),
      value: SearchType.image,
    ),
    DropdownMenuItem<SearchType>(
      child:Text('News'),
      value: SearchType.news,
    ),
    DropdownMenuItem<SearchType>(
      child:Text('Shopping'),
      value: SearchType.shopping,
    ),
  ],
  onChanged: (SearchType val) => _searchType = val,
),

Putting the form widgets together

It’s cool that we have all of these different types of fields that look good and work great. But you will often want them to be grouped together so that they can be somewhat controlled as a group. You’ll do this with a Form widget.

Form widget

As with HTML, you can live just fine without a Form widget. It is a convenience widget with no visual component. That is to say you never actually see it rendered on the device. Its only purpose is to wrap all of its inputs, thereby grouping them – and their data – into a unit. It does so using a key. Remember that we introduced keys in the last chapter and told you that except in a few situations, keys can be ignored. This is one place where keys are needed. If you decide to use a Form, you need a GlobalKey of type FormState:
GlobalKey<FormState> _key = GlobalKey<FormState>();
You’ll set that key as a property to your form:
@override
Widget build(BuildContext context) {
  return Form(
    key: _key,
    autovalidate: true,
    child: // All the form fields will go here
  );
}
At first glance, the Form doesn’t seem to change anything. But a closer look reveals that we now have access to
  • autovalidate: a bool. True means run validations as soon as any field changes. False means you’ll run it manually. (We’ll talk about validations in a few pages.)

  • The key itself which we called _key in the preceding example.

That _key has a currentState property which in turn has these methods:
  1. 1.

    save()– Saves all fields inside the form by calling each’s onSaved

     
  2. 2.

    validate()– Runs each field’s validator function

     
  3. 3.

    reset()– Resets each field inside the form back to its initialValue

     

Armed with all this, you can guess how the Form groups the fields nested inside of it. When you call one of these three methods on FormState, it iterates the inner fields and calls that method on each. One call at the Form level fires them all.

But hang on a second! If _key.currentState.save() is calling a field’s onSaved(), we need to provide an onSaved method. Same with validate() calling the validator. But the TextField, Dropdown, Radio, Checkbox, and Slider widgets themselves don’t have those methods. What do we do now? We wrap each field in a FormField widget which does have those methods. (And the rabbit hole gets deeper.)

FormField widget

This widget’s entire purpose in life is to provide save, reset, and validator event handlers to an inner widget. The FormField widget can wrap any widget using a builder property:
FormField<String>(
  builder: (FormFieldState<String> state) {
    return TextField(); // Any field widget like DropDownButton,
                        // Radio, Checkbox, or Slider.
  },
  onSaved: (String initialValue) {
    // Push values to a repository or something here.
  },
  validator: (String val) {
    // Put validation logic here (further explained below).
  },
),

So we first wrap a FormField widget around each input widget, and we do so in a method called builder. Then we can add the onSaved and validator methods.

Tip

Treat a TextField differently. Instead of wrapping it, replace it with a TextFormField widget if you use it inside a Form. This new widget is easy to confuse with a TextField but it is different. Basically ...

TextFormField = TextField + FormField

The Flutter team knew that we’d routinely need a TextField widget in combination with a FormField widget so they created the TextFormField widget which has all of the properties of a TextField but adds an onSaved, validator, and reset:

TextFormField(
  onSaved: (String val) {
    print('Search Term TextField: form saved $val');
  },  validator: (String val) {
    // Put your validation logic here
  },
),

Now isn’t that nicer? Finally we catch a break in making things easier. Checkboxes don’t have this feature. Nor do Radios nor Dropdowns. None except TextFields.

Best practice: Text inputs without a Form should always be a TextField. Text inputs inside a Form should always be a TextFormField.

onSaved

Please remember that your Form has a key which has a currentState which has a save() method. Got all that? No? Not super clear? Let’s try it this way; on a “Save” button press, you will write your code to call ...
_key.currentState.save();

... and it in turn invokes the onSaved method for each FormField that has one.

validator

Similarly, you probably guessed that you can call ...
_key.currentState.validate();

... and Flutter will call each FormField’s validator method. But there’s more! If you set the Form’s autovalidate property to true, Flutter will validate immediately as the user makes changes.

Each validator function will receive a value – the value to be validated – and return a string. You’ll write it to return null if the input value is valid and an actual string if it is invalid. That returned string is the error message Flutter will show your user.

Validate while typing

Remember that the way to perform instant validation is to set Form.autovalidate to true and write a validator for your TextFormField:
return Form(
 autovalidate: true,
 child: Container(
  TextFormField(
    validator: (String val) {
     // Let's say that an empty value is invalid.
     if (val.isEmpty)
      return 'We need something to search for';
     return null;
    },
  ),
 ),
);

Obviously it makes no sense to validate a DropdownButton, Radio, Checkbox, Switch, or Slider while typing because you don’t type into them. But less obviously, it does not work with a TextField inside of a FormField. It only works with a TextFormField. Strange, right?

Tip

Again, best practice is to use a TextFormField. But if you insist on using a TextField inside a FormField, you can brute force set errorText like this:

FormField<String>(
 builder: (FormFieldState<String> state) {
  return TextField(
   controller: _emailController,
   decoration: InputDecoration(
    // This says if the value looks like an email
    set errorText
    // to null. If not, display an error message.
    errorText:
      RegExp(r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+.
      [a-zA-Z]+')
      .hasMatch(_emailController.text)
       ? null
       : "That's not an email address",
   ),
  );
 },
),

Validate only after submit attempt

There are times when you don’t want your code to validate until the user has finished entering data. You should first set autovalidate to false. Then call validate() in the button’s pressed event:
RaisedButton(
 child: const Text('Submit'),
 onPressed: () {
  // If every field passes validation, run their save methods.
  if (_key.currentState.validate()) {
   _key.currentState.save();
   print('Successfully saved the state.')
  }
 },
)

One big Form example

I know, I know. This is pretty complex stuff. It might help to see these things in context – how they all fit together. Below you’ll find a fully commented example ... a big example. But as big as it is, it was originally much larger. Please look at our online source code repository for the full example. Hopefully they will help your understanding of how Form fields relate.

Let’s say that we wanted to create a scene for the user to submit a Google-like web search. We’ll give them a TextFormField for the search String, a DropdownButton with the type of search, a checkbox to enable/disable safeSearch, and a button to submit:
enum SearchType { web, image, news, shopping }
// This is a stateful widget. Don't worry about how it or
// the setState() calls work until
// Chapter 9. For now, just focus on the Form itself.
class ProperForm extends StatefulWidget {
 @override
 _ProperFormState createState() => _ProperFormState();
}
class _ProperFormState extends State<ProperForm> {
 // A Map (aka. hash) to hold the data from the Form.
 final Map<String, dynamic> _searchForm = <String, dynamic>{
  'searchTerm': ",
  'searchType': SearchType.web,
  'safeSearchOn': true,
 };
 // The Flutter key to point to the Form
 final GlobalKey<FormState> _key = GlobalKey();
 @override
 Widget build(BuildContext context) {
  return Form(
   key: _key,
   // Make autovalidate true to validate on every keystroke. In
   // this case we only want to validate on submit.
   //autovalidate: true,
   child: Container(
    child: ListView(
     children: <Widget>[
      TextFormField(
       initialValue: _searchForm['searchTerm'],
       decoration: InputDecoration(
        labelText: 'Search terms',
       ),
       // On every keystroke, you can do something.
       onChanged: (String val) {
        setState(() => _searchForm['searchTerm'] = val);
       },
       // When the user submits, you could do something
       // for this field
       onSaved: (String val) { },
       //Called when we "validate()". The val is the String
       // in the text box.
       //Note that it returns a String; null if validation passes
       // and an error message if it fails for some reason.
       validator: (String val) {
        if (val.isEmpty) {
         return 'We need something to search for';
        }
        return null;
       },
      ),
      FormField<SearchType>(
       builder: (FormFieldState<SearchType> state) {
        return DropdownButton<SearchType>(
         value: _searchForm['searchType'],
         items: const <DropdownMenuItem<SearchType>>[
          DropdownMenuItem<SearchType>(
           child: Text('Web'),
           value: SearchType.web,
          ),
          DropdownMenuItem<SearchType>(
           child: Text('Image'),
           value: SearchType.image,
          ),
          DropdownMenuItem<SearchType>(
           child: Text('News'),
           value: SearchType.news,
          ),
          DropdownMenuItem<SearchType>(
           child: Text('Shopping'),
           value: SearchType.shopping,
          ),
         ],
         onChanged: (SearchType val) {
          setState(() => _searchForm['searchType'] = val);
         },
        );
       },
       onSaved: (SearchType initialValue) {},
      ),
      // Wrapping the Checkbox in a FormField so we can have an
      // onSaved and a validator
      FormField<bool>(
       //initialValue: false, // Ignored for Checkboxes
       builder: (FormFieldState<bool> state) {
        return Row(
         children: <Widget>[
          Checkbox(
           value: _searchForm['safeSearchOn'],
           // Every time it changes, you can do something.
           onChanged: (bool val) {
            setState(() => _searchForm['safeSearchOn'] = val);
           },
          ),
          const Text('Safesearch on'),
         ],
        );
       },
       // When the user saves, this is run
       onSaved: (bool initialValue) {},
       // No need for validation because it is a checkbox. But
       // if you wanted it, put a validator function here.
      ),
      // This is the 'Submit' button
      RaisedButton(
       child: const Text('Submit'),
       onPressed: () {
        // If every field passes validation, let them through.
        // Remember, this calls the validator on all fields in
        // the form.
        if (_key.currentState.validate()) {
         // Similarly this calls onSaved() for all fields
         _key.currentState.save();
         // You'd save the data to a database or whatever here
         print('Successfully saved the state.');
        }
       },
      )
     ],
    ),
   ),
  );
 }
}

Conclusion

It takes a while to understand Flutter forms. Please don’t be discouraged. Look over the preceding example a couple more times and write a little code. It begins to make sense very quickly. And while the topic of Forms might have been a little intimidating to you, Images, Icons, and Text were very straightforward, right?

In the next chapter, we’ll start to see our app come alive because we’re going to learn about creating all the different kinds of buttons and making them – or any widget for that matter – respond to taps and other gestures!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.108.105