If you’re going to make apps in Flutter, you’re going to be using TextFormField widgets. They allow users to enter data, and they’re used a lot. Best to get to know a little more about this widget.
When a user enters data into a Flutter app, it’s a common requirement for that data be validated before it’s saved to a database for example. If you don’t need to validate the text input at all, you do have the option to use the widget, TextField. However in most cases, you do need to validate user input, and so it’s suggested you use a TextFormField widget with it’s property, validator. Know, however, a TextFormField is just a TextField widget wrapped in a FormField widget allowing it then to work with a Form widget.
Involving a Form widget in your app simply makes it easier to validate, reset, and save multiple input fields at once. Note, TextFormFeild widgets can work without a Form widget. To do so, a GlobalKey is passed to each TextFormField constructor instead allowing the property, GlobalKey.currentState, to also validate, reset and save an input field. We’ll get into that later. For now, let’s take a look at the widget, TextFormField.
I Like Screenshots. Click For Gists.
As always, I prefer using screenshots over gists to show concepts rather than just show code in my articles. I find them easier to work with frankly. However, you can click or tap on these screenshots to see the code they represent in a gist or in Github. Ironically, it’s better to read this article about mobile development on your computer than on your phone. Besides, we program on our computers — not on our phones. For now.
No Moving Pictures, No Social Media
There will be gif files in this article demonstrating aspects of the topic at hand. However, it’s said viewing such gif files is not possible when reading this article on platforms like Instagram, Facebook, etc. They may come out as static pictures or simply blank placeholder boxes. Please, be aware of this and maybe read this article on medium.com
Learn By Example
We’ll use a Cookbook example to examine the TextFormField widget. In this case, its the example called, Build a form with validation. I’ve extracted that example and suppled it to you as a gist called, form_validation.dart. Please take a copy to follow along.
A screenshot of that gist file is displayed below. Highlighted is the TextFormField widget enclosed inside a Form widget. We’ll first examine how these two widgets interact with each other.
FORM A FIELD
To do so, we’ll first look at a snapshot below conveying the class hierarchy of the TextFormField widget. At a glance, you’ll realize now a TextFormField is a StatefulWidget. That means it can have a ‘changing state.’ In other words, it retains a single value in this case that may change over time with the use of its accompanying State object. In this case, this value is stored in an instance variable called, _value.
Let’s take a look at that State object. In the Flutter framework, it’s named, FormFieldState<T>. It’s a Generic class. However, when implemented as a TextFormField widget, the type used is of type, String. And so, the value stored and possibly changed over time in a TextFormField is a String value. The screenshot below displays the instance variable that stores that String value. Note, we’ll later examine how all the getters highlighted below are used.
And so, in the Cookbook example above, when the TextFormField is eventually displayed on a computer screen, you know the build() function found in the State object, FormFieldState<String>, is to run. See below.
Note the last two lines highlighted. The first line tells you how the TextFormField is ‘registered’ with the enclosing Form widget. In fact, it tells you the class, Form, is obviously defined in the same library file as that State object since the method, _register(), begins with an underscore.
In fact, scrolling up in that library file called, form.dart, you’ll find the Form widget is also a StatefulWidget with its own State object called, FormState. That State object has a private instance variable called, _fields, which collects all the TextFormFields enclosed in the Form widget. Below is a screenshot now of that _register() function.
What’s Your Field?
If you examine closely what’s involved when registering a TextFormField widget to a Form widget, you see the Set variable called, _fields, is adding all the State objects of type, FormFieldState, from all the TextFormField widgets enclosed by the ‘nearest’ Form widget in the widget tree.
In the screenshot above, the static method, Form.of(context), looks up the widget tree for a Form widget's State object. Note, the Conditional Member Access operator, ?., is used just in case such a State object isn’t found. In other words, if a Form widget is not currently being used. As stated above, this tells you that TextFormField widgets can be used without being enclosed in a Form widget.
However, if it is found, the TextFormField’s state object is passed to the method, _register(). Note, the method takes in the state object as the generic class, FormFieldState, and doesn’t care about the type. In fact, it can be any type. Hence the keyword, dynamic. It’s not concerned with the value in which the TextFormField widgets store at this point.
Below is a screenshot of the Form widget's State object, FormState, with its Set instance variable, _fields. Using a Set variable assures a TextFormField’s State object is only added once — and doesn’t ‘blow up’ if such an object is not in the Set when there’s an attempt to remove one. It’s an effective way to collect such objects. This variable, _fields, is used to iterate through the collected objects during validation and saving operations. You’ll see this shortly.
The next line in that build() function is somewhat interesting because of where you’ll find this ‘builder’ is defined. It’s defined right inside the TextFormField widget’s constructor! Looking at that line below, you can see the executing function is offered by the accompanying StatefulWidget using the keyword, widget. Well, let’s see where that StatefulWidget gets this builder property from.
Be The Builder
A quick peek at the TextFormField’s StatefulWidget (its parent, FormField), we see that function is a required named parameter called, builder. In Dart, functions are also objects, and the typedef, or function-type alias, defined for the named parameter, builder, describes an object as a function called, FormFieldBuilder<T>. As it happens, it takes in that StatefulWidget’s accompanying State object and returns a Widget. You can see that in the second screenshot below.
Are you following so far? Note, the typedef is a generic function, but again, when implemented a TextFormField, the type specified is of type String. The interesting thing is that you’ll find that function is defined in the TextFormField’s constructor (see below) and returns a widget of type, TextField. Tap/Click on the screenshot below to view the code in Github and get a better appreciation.
Essentially, this widget defines itself in its constructor and that definition is then returned in its State object’s build() function (see below). I personally haven’t found this to be a common arrangement in Flutter’s many other widgets. Interesting no?
A Form Of Validation
Let’s get back to the cookbook example. It’s a very very simple example with one TextFormField widget. It simply validates if you’ve typed something in the TextFormField or not. We’ll run through how that’s done using the TextFormField widget and the Form widget. You’re going to see that Set variable, _fields, used to iterate through the collected objects.
When you hit the Submit button, the example will take you to the anonymous function defined below in the RaisedButton widget. You can see in the screenshot below, it utilizes the Form’s State object and calls that object’s validate() function. Let’s see what all that involves.
A Global State Reference
As an aside, note how the Global key, _formKey, is used to retrieve the Form widget’s State object in this simple example. Let’s talk about this a little bit.
Looking at the abstract class, GlobalKey, in the first screenshot below, you see it’s a generic class of a type that extends a State object. Therefore, as you see in the second screenshot below, the GlobalKey class has a getter called, currentState, that returns that type. As it happens, retrieving a specified State object is primarily why one would use a GlobalKey.
That’s all I’ll say about that for now. Let’s continue with the validation process.
A Valid Form
In the screenshot below, you see the validate() function, in fact, calls its ‘private’ counterpart which iterates through all the ‘TextFormField’ State objects collected in our ol’ friend, _fields. As you see, each State object, in turn, calls its own validate() function — all returning a boolean value.
Again, the keyword, dynamic, is used above. The type of value stored is just not a consideration at this point. Looking now into each FormFieldState object (i.e. each TextFormField widget), let’s look inside the validate() function and again see a ‘private’ variation of the function is called. In turn, it calls the property, validator, in the corresponding StatefulWidget object. Note, it tests that the property, validator, is not null first — telling you it’s optional.
Notice above the instance variable, _value, is passed as a parameter to that validator property. Thus, it obviously represents a function. Further note above the boolean expression returned from the validate() function is !hasError. It uses the getter, hasError, and that getter tests if the String, _errorText, is not null. How is that String variable not null? The plot thickens.
Lastly, notice the setState() function in the validate() function displayed above. As you may know, calling a State object’s setState() function will cause its corresponding build() function to be called soon after. Now, why is that, you wonder? That’s because the result of the validation that occurs may require it to. That’s why. Let’s demonstrate that.
In the ‘private’ function, _validate, it tests to see if the StatefulWidget’s ‘validator’ property is not null. In the Cookbook example (see below), that property is not null and is defined as an anonymous function. It’s passed to that property as the named parameter, validator. As you see in the screenshot below, that anonymous function is to test if the passed value (the field’s value) is empty or not. It will return null if the validation operation passes, or an ‘error message’ if the validation results in error. See how that all works?
And so, for example, if you simply tap on the Submit button without typing out anything, the string, ‘Please enter some text’, is passed to the instance variable, _errorText. See below.
If you remember, the getters, hasError and errorText, have access to that variable near the start of the class, FormFieldState<T>. See below.
It is the getter, errorText, that is referenced in the TextFormField’s constructor (see below) when defining the returned widget, TextField. And so, when you don’t type anything, you see in red the error message returned from your anonymous function, Please enter some text. See how that all works now?
Change Your Value
Let’s further examine the process involved when you’re typing into a TextFormField widget. Realize now, that every time you type a character on the keyboard when filling in a TextFormField widget, the method, didChange(), displayed below fires updating that String value it stores in the instance variable, _value. Agan, every time you press a key of the keyboard.
Let’s Make A Change
Let’s modify the simple example a little bit and include the appropriate anonymous function to the named parameter, onChanged. You’re going to discover this function will also be called every time you type in a character on the keyboard. This function will be passed a String value as a parameter containing what has been typed in so far. In this case, we’ll have this function print that value to the IDE’s console window. It’s a simple little exercise.
Type A Test
Let’s say we type into the TextFormField the sentence, ‘This is a Test.’ The two screenshots below depict our progress while typing in the sentence and then the end result. The first screenshot is frozen right when the letter, s, was typed on the keyboard. Got that so far?
That means, at that instance, the TextFormField’s State object, FormFieldState, has called the method, didChange, and takes in the String value, ‘This is a Tes’, into its instance variable, _value. Moments before that, the variable, _value, had been assigned the value, ‘This is a Te’. The method didChange() gets called with every tap of the keyboard. Note, it’s all enclosed in a setState() function, and so the progression is displayed on the screen as we proceed.
However, this was not before the TextFormField’s onChanged() function was also called and passed the current String value. Below is a screenshot of the onChangedHandler() method. It tests that the onChanged property is not null (it’s now not null in our little exercise) and so, as you can see, the onChanged() function is also passed the String value and called before the TextFormField’s State object, field, calls its didChange() function.
And so, as you see in the next two screenshots below, the print() function is called and prints out in the IDE’s console window the progression of characters typed out into the TextFormField. If and when you would use the onChanged parameter in your own TextFormField widgets, of course, depends on your app’s own requirements.
Where does this onChangedHandler() function come from anyway?! Interestingly, it comes from that constructor again. The TextFormField’s constructor not only returns the widget, TextField, but defines and supplies the onChangedHandler() function to that TextField widget’s named parameter, onChanged. Again, so far as I’m aware, this is an approach I have not seen in any other widget in the Flutter framework.
Your Text Has Field
Let’s now take a look at a few of the named parameters concerning the TextFormField widget. You may have by now realized, the many parameters passed into the TextFormField widget are actually intended for the TextField widget defined in the TextFormField’s constructor. We’ll look at all those when we cover the widget, TextField, other time in a separate article. The screenshot below is the TextFormField widget’s list of parameters and part of its constructor all presented in two columns. You can see the majority of parameters indeed further go on to the TextField widget. We’ll just look at that a few of interest that relate specifically with the TextFormField widget and finish this up.
The first few parameters includes the named parameter, key. This makes sense. The TextFormField widget extends a StatefulWidget after all. You’re then free to pass a Key object to your TextFormField’s when you want to retrieve such StatefulWidgets in your testing for example. Otherwise, in some instances, you would supply a new key at some point, the associated State object is then re-created in the StatefulWidget lifecycle. Finally, as stated in the beginning, you would pass in a GlobalKey as a key to the TextFormField widget instead of using Form widget app to preform your validation and such. Regardless, you have the Key parameter.
The next parameter however is an interesting option to pass into your TextFormField widget — because if you do, you’re then not supposed to provide an initial value using the next following parameter called, initalValue. I’ll explain this.
The first of the TextFormFields’ many assert statements that test the parameter values while developing your app, dictates you can pass either an initial value or the controller object, but not both. Passing both while you’re developing will cause an error in that assert statement. See below.
Your Initial Value
Now with either option, the same result ends with a value passed to the TextFormField’s parent class, FormField<T>, to the named parameter, initalValue. We’ll now walk through the process involved to get this done.
And so, in the TextFormField’s constructor, we see the named parameter, initialValue, in the parent class is assigned either the property, text, from a passed controller or the parameter, initialValue, if it’s passed. Otherwise, an empty String is assigned. Do you see that below? The if null operator, ??, tests if initialValue is null or not. If null, an empty String is assigned.
The testing for null doesn’t end there, however. In the TextFormField’s State object, _TextFormFieldState, it too looks to see if a controller was passed. If not, it creates a TextEditingController object inside its initState() function using the value it then assumes was passed at this point in the parameter, initalValue, instead. Do you see that now? It’s either one or the other. Not both.
Save Your Form
To finish up, let’s quickly look at the named parameter, onSaved. Again, we’re concerned really with the parameters passed to the TextFormField’s parent class, FormField<T>, and we’ve touched on most of them now. The rest are somewhat self-explanatory. I’ll let you look at them yourself. And so, what’s this parameter, onSave, all about?
Save Your Text
You can see in the screenshot of the TextFormField below its named parameter, onSaved, is then passed to its parent class, FormField. It’s a function that’s supplied to the named parameter, onSaved.
The function is defined as follows,
void Function(T newValue) , and assigned to the typedef (function-type alias),
FormFieldSetter<T>. You see it involves a generic type, but of course, when used by a TextFormField widget, this means the function takes in a value of type String. And note, it is the String the user has finished entering and not a progressive iteration of every character typed in on the keyboard.
In our simple example, let’s place an anonymous function to display the finished String value in a Snackbar. This Snackbar will then appear when the user presses the Submit button.
Form A Save
To invoke that function, however, it involves the Form widget once again. You would have to include an additional function found in the Form widget’s State object, FormState, and guess what its name is? It’s been included in the screenshot of the simple example below placed where I would say makes sense in most cases — after the Form has validated its collection of TextFormField widgets.
Looking into that save() function, we see our ol’ friend the Set variable, _fields, once again iterating through the collected TextFormField State objects. However, this time calling, in turn, their own save() function.
In each TextFormField’s State object, their own save() function checks if the ‘onSaved’ property is not null. If it is not null, the getter, value, is called to supply the String value each State object currently stores.
In our simple example, after passing through the form validation, each TextFormField involved in the Form (in this case only one) has it’s onSaved() function passed the resulting String value if the it’s onSaved() function is defined. In this case, the value is then displayed in the Snackbar. What other operations can be done at this point? One prominent example would be saving that final value to a database — and there you have it.
Let’s leave it there. I feel ‘looking under the hood’ of your Flutter widgets will only benefit you in your furture endeavours. Maybe you’ll even develop your own widgets that return the bulk of themselves from their very constructors.