Handling text input
Learning objectives
- You know how to create and use a textfield that can be used to type in text.
TextField and Decoration
Flutter provides a Widget called TextField for providing text input. The following example shows an application with a TextField
. Opening up the application, we see no guidelines or other instructions that would help us decide what to do with the textfield.
The TextField widget has a property decoration
that is given an instance of the InputDecoration class. With InputDecoration, we can e.g. provide input hints and define borders. The following example outlines the use of decoration
. When the application is launched, the textfield has the text "Name" that disappears when we start typing text to it. In addition, the textfield has a border.
Monitoring for changes
The TextField has a property onChanged
that is given a method that is called whenever the value in the TextField changes. The method receives the text from the textfield as a parameter. In the following example, whenever we write something to the textfield, the contents of the textfield are printed to the console.
We can also include state management into the application. We could, for example, store the written text in a provider, and use the text elsewhere in the application. The following example outlines an application with a textfield, where changes to the textfield are reflected in another widget.
Controlling text in TextField
The onChange
property provides a convenient way to monitor changes to the TextField
and is suitable for basic use cases where we simply need to monitor for changes. It has its limitations though; as an example, we cannot influence the text shown in the TextField.
To programmatically control what is shown in the TextField, we use a TextEditingController. The TextEditingController
contains the value of the TextField that it is associated with, and can be used to change the value as well as to listen for changes in the value. The TextEditingController
is given as a parameter to the property controller
of a TextField. The following example shows how a TextEditingController
is taken into use.
The TextEditingController has a set of properties, which can be used to modify the shown text. For example, the property text
provides access to the text in the TextField. The following example shows an application that has a TextField and a Button. Pressing the button clears the text from the textfield.
The Expanded
widget is used with TextField for similar reasons as we have used it with Rows and Containers. If you try to run the the above example without Expanded
, you'll notice that the application crashes -- this stems from the TextField
attempting to take an infinite amount of space.
TextFieldController and Riverpod
In the following example, we create an application that allows typing in data. The data is added to a list whenever the Add
button is pressed, which also leads to the text field being emptied. The list is maintained by Riverpod, and changes to it are reflected in another widget that displays the list.
Note that when TextEditingController
is used in an application where widgets are disposed of, e.g. when a screen is removed, it is a good practice to dispose of the TextEditingController
as well. For this, we need to use a widget that maintains a state, as widgets with state have a method dispose
that is called when the widget is being removed. The ConsumerWidget
is stateless, and thus, we need to resort to another option with state.
Riverpod comes with a pair of widgets, which behave similar to Flutter's StatefulWidget
; there is a ConsumerStatefulWidget and a ConsumerState. Using them, the above InputForm
would be implemented as follows, taking care of the _controller
when the widget is disposed of.
As you may notice, when using ConsumerStatefulWidget
and ConsumerState
, the WidgetRef
is inherited from the ConsumerState
, which means that it is available in all of the methods of the class extending ConsumerState
.
In addition to the dispose
method, stateful widgets have a method initState
which is used to initiate the state of the widget. This method would also be an appropriate place to adjust the behavior of the TextEditingController
.
We can, for example, define how we wish to listen to the TextEditingController
in the initState
method by adding a listener to the TextEditingController
. Adding a listener is done with the addListener
method of TextEditingController
. In the following example, whenever something happens with the TextField
the TextEditingController
is linked with, the text in the TextField is printed to the console.
We could also, for example, modify the application so that uppercase characters are forbidden.
In the above example, we set the value
property of the TextEditingController
, copying it from itself and setting a new text into it. This is done as the TextEditingController
also keeps track of the cursor position -- by copying the value
, we maintain the position of the cursor.
Note that while initState
is used to set up widget behavior and widget state, it cannot directly used to watch for changes in the state. As an example, the following would not work.
// ..
void initState() {
super.initState();
final itemCount = ref.watch(listProvider).length;
// doing something with the item count
}
// ...
This limitation stems from the state not yet being initiated (i.e. the method initState
has not yet finished), and thus the state cannot yet be monitored (and reacted to). If there is a need to read a provider in the initState
, one possibility is to use ref.read()
instead of ref.watch()
. The following would work.
// ...
final listProvider = StateProvider<List<String>>((ref) => []);
// ...
void initState() {
super.initState();
final itemCount = ref.read(listProvider).length;
// doing something with the item count
}
Another option is to use a post frame callback, which would execute after the method has been finished.
// ...
final listProvider = StateProvider<List<String>>((ref) => []);
// ...
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final itemCount = ref.watch(listProvider).length;
// doing something with the item count
});
}