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
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
The Image widget
- 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.
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.
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.
Network images
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
BoxFit options
fill | Stretch it so that both the width and the height fit exactly. Distort the image | |
cover | Shrink or grow until the space is filled. The top/bottom or sides will be clipped | |
fitHeight | Make the height fit exactly. Clip the width or add extra space as needed | |
fitWidth | Make the width fit. Clip the height or add extra space as needed | |
contain | Shrink until both the height and the width fit. There will be extra space on the top/bottom or sides |
Photo courtesy of Eye for Ebony on Unsplash
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
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.
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
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.
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
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.
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.
Sliders
Dropdowns
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
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.
- 1.
save()– Saves all fields inside the form by calling each’s onSaved
- 2.
validate()– Runs each field’s validator function
- 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
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:
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
... and it in turn invokes the onSaved method for each FormField that has one.
validator
... 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
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:
Validate only after submit attempt
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.
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!