Flutter Text Fields: Mastering User Input and Form Handling
Text fields are the backbone of user interaction in mobile apps. Whether you're building a login form, a search bar, or a comment section, understanding how to work with text input in Flutter is essential. In this article, we'll explore the TextField widget, learn how to handle user input, validate forms, and create delightful input experiences.
Understanding the TextField Widget
At its core, a TextField is a widget that allows users to enter text. Flutter provides a powerful and flexible TextField widget that handles everything from basic text input to complex scenarios like password fields, multiline input, and formatted text.
Let's start with a simple example:
TextField(
decoration: InputDecoration(
labelText: 'Enter your name',
hintText: 'John Doe',
border: OutlineInputBorder(),
),
)
This creates a basic text field with a label, hint text, and an outlined border. The TextField widget automatically handles keyboard display, text selection, and basic editing operations.
TextField Structure
Controlling Text Input
To actually use the text that users enter, you need to control the TextField's value. Flutter provides two approaches: controlled (using a TextEditingController) and uncontrolled (using callbacks).
Here's how to use a TextEditingController:
class MyForm extends StatefulWidget {
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Username',
border: OutlineInputBorder(),
),
onChanged: (value) {
print('Current text: $value');
},
);
}
}
The TextEditingController gives you full control over the text field's value. You can read the current text with _controller.text, set it programmatically with _controller.text = 'New value', or listen to changes using the controller's addListener method.
Remember to dispose of the controller when you're done with it to prevent memory leaks. This is why we override the dispose method in the State class.
Common TextField Configurations
Flutter's TextField is highly customizable. Let's explore some common configurations:
Password Fields
For password input, you'll want to obscure the text:
TextField(
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
)
You can also add a toggle button to show/hide the password:
class PasswordField extends StatefulWidget {
@override
_PasswordFieldState createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
bool _obscureText = true;
@override
Widget build(BuildContext context) {
return TextField(
obscureText: _obscureText,
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(_obscureText ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
),
);
}
}
Multiline Input
For longer text input, like comments or descriptions, use multiline mode:
TextField(
maxLines: null,
minLines: 3,
decoration: InputDecoration(
labelText: 'Your message',
border: OutlineInputBorder(),
),
)
Setting maxLines: null allows the field to expand as the user types. The minLines property sets the initial height.
Input Types and Keyboards
You can specify the type of input expected, which helps Flutter show the appropriate keyboard:
TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
)
TextField(
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Age',
border: OutlineInputBorder(),
),
)
TextField(
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: 'Phone number',
border: OutlineInputBorder(),
),
)
Flutter provides various keyboard types including TextInputType.emailAddress, TextInputType.number, TextInputType.phone, and TextInputType.url.
Input Validation
Validating user input is crucial for creating robust forms. The TextField widget provides a validator property, but it's typically used with a Form widget for proper validation handling.
Here's a complete example with form validation:
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!value.contains('@')) {
return 'Please enter a valid email';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
print('Email: ${_emailController.text}');
print('Password: ${_passwordController.text}');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: _validateEmail,
),
SizedBox(height: 16),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
validator: _validatePassword,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _submitForm,
child: Text('Login'),
),
],
),
);
}
}
Notice that we're using TextFormField instead of TextField. TextFormField is a specialized version that integrates seamlessly with the Form widget. The validator function returns a String if there's an error, or null if the input is valid.
Form Validation Flow
Styling Text Fields
The InputDecoration widget provides extensive styling options. Let's look at some common styling patterns:
TextField(
decoration: InputDecoration(
labelText: 'Search',
hintText: 'Type to search...',
prefixIcon: Icon(Icons.search),
suffixIcon: IconButton(
icon: Icon(Icons.clear),
onPressed: () {
_controller.clear();
},
),
filled: true,
fillColor: Colors.grey[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.blue),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.red),
),
),
)
You can customize borders for different states: enabledBorder for the default state, focusedBorder when the field is active, and errorBorder when validation fails. The prefixIcon and suffixIcon properties let you add icons to enhance the user experience.
Advanced Input Handling
Text Input Formatters
Sometimes you need to format or restrict input as the user types. Flutter provides TextInputFormatter for this purpose:
import 'package:flutter/services.dart';
TextField(
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
LengthLimitingTextInputFormatter(10),
],
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Phone number',
border: OutlineInputBorder(),
),
)
You can also create custom formatters. Here's an example that formats a phone number:
class PhoneNumberFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final text = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
if (text.length <= 3) {
return TextEditingValue(text: text);
} else if (text.length <= 6) {
return TextEditingValue(
text: '${text.substring(0, 3)}-${text.substring(3)}',
selection: TextSelection.collapsed(offset: text.length + 1),
);
} else {
return TextEditingValue(
text: '${text.substring(0, 3)}-${text.substring(3, 6)}-${text.substring(6)}',
selection: TextSelection.collapsed(offset: text.length + 2),
);
}
}
}
TextField(
inputFormatters: [PhoneNumberFormatter()],
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: 'Phone',
border: OutlineInputBorder(),
),
)
Focus Management
Managing focus is important for creating smooth form experiences. You can programmatically move focus between fields:
Focus Flow Between Fields
When the user presses "Next" on the keyboard, focus automatically moves to the next field, creating a smooth input experience.
class MultiFieldForm extends StatefulWidget {
@override
_MultiFieldFormState createState() => _MultiFieldFormState();
}
class _MultiFieldFormState extends State<MultiFieldForm> {
final _field1Focus = FocusNode();
final _field2Focus = FocusNode();
final _field3Focus = FocusNode();
@override
void dispose() {
_field1Focus.dispose();
_field2Focus.dispose();
_field3Focus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
focusNode: _field1Focus,
textInputAction: TextInputAction.next,
onSubmitted: (_) {
_field1Focus.unfocus();
FocusScope.of(context).requestFocus(_field2Focus);
},
decoration: InputDecoration(labelText: 'Field 1'),
),
TextField(
focusNode: _field2Focus,
textInputAction: TextInputAction.next,
onSubmitted: (_) {
_field2Focus.unfocus();
FocusScope.of(context).requestFocus(_field3Focus);
},
decoration: InputDecoration(labelText: 'Field 2'),
),
TextField(
focusNode: _field3Focus,
textInputAction: TextInputAction.done,
decoration: InputDecoration(labelText: 'Field 3'),
),
],
);
}
}
The textInputAction property determines what action button appears on the keyboard. Setting it to TextInputAction.next shows a "Next" button, while TextInputAction.done shows a "Done" button. The onSubmitted callback fires when the user presses this button.
Best Practices
Here are some tips for working with text fields effectively:
- Always dispose controllers and focus nodes: This prevents memory leaks and ensures your app performs well.
- Provide clear labels and hints: Help users understand what input is expected.
- Use appropriate keyboard types: Make it easier for users to enter the right type of data.
- Validate input early: Show validation errors as users type or when they move to the next field.
- Handle edge cases: Consider what happens with very long text, special characters, or empty input.
- Test on different devices: Keyboard behavior can vary between iOS and Android.
Conclusion
Text fields are a fundamental part of Flutter app development. By understanding how to use TextField and TextFormField widgets, manage controllers, validate input, and style your forms, you can create excellent user input experiences. Remember to always dispose of your controllers and focus nodes, and test your forms thoroughly across different devices and scenarios.
As you build more complex forms, you'll discover that Flutter's text input system is both powerful and flexible, allowing you to create exactly the user experience your app needs. Happy coding!