TextField
Understand how to create and manage text input fields in Flutter.
What is it?
TextField is a material design widget that allows users to enter text into your Flutter application. It provides a wide range of customization options including styling, validation, keyboard types, input formatting, and focus management. TextField is one of the most commonly used input widgets in Flutter applications.
Why does it exist?
TextField exists to:
- Collect user text input
- Support various input types (text, numbers, emails, etc.)
- Provide visual feedback for user interaction
- Enable form validation and error handling
- Support input formatting and masking
- Manage focus and keyboard behavior
- Create accessible and usable input fields
Basic TextField
Creating a simple text input field.
// Import required packages
import 'package:flutter/material.dart';
/// Basic TextField example
class BasicTextFieldExample extends StatefulWidget {
const BasicTextFieldExample({super.key});
@override
State<BasicTextFieldExample> createState() => _BasicTextFieldExampleState();
}
class _BasicTextFieldExampleState extends State<BasicTextFieldExample> {
// Controller to manage the text field's state
// This gives us access to the text content and allows us to control the field programmatically
final TextEditingController _controller = TextEditingController();
// Variable to store the current text value
String _currentText = '';
@override
void initState() {
super.initState();
// Add a listener to the controller to be notified when the text changes
// This is called whenever the user types or the text is programmatically changed
_controller.addListener(() {
// Update the current text when the controller's text changes
setState(() {
_currentText = _controller.text;
});
});
}
@override
void dispose() {
// Always dispose controllers when the widget is removed to prevent memory leaks
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Basic TextField'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Basic TextField with default styling
// This is the simplest form of TextField
TextField(
// The controller manages the text state
controller: _controller,
// Called when the user submits the text (presses done/enter)
// This is useful for form submission or search actions
onSubmitted: (value) {
print('Submitted: $value');
},
// Called when the text changes (every keystroke)
// This is useful for real-time validation or updates
onChanged: (value) {
// This is also called when the text changes
// The controller listener above handles this as well
},
),
const SizedBox(height: 16),
// Display the current text
// This shows how to access the text from the controller
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Current Text:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
_currentText.isEmpty ? '(empty)' : _currentText,
style: const TextStyle(fontSize: 16),
),
],
),
),
],
),
),
);
}
}
What's happening here? - TextEditingController manages text state - controller.text accesses current text - addListener listens for text changes - dispose prevents memory leaks
TextField with Decoration
Styling TextField with InputDecoration.
/// TextField with various decorations
class DecoratedTextFieldExample extends StatefulWidget {
const DecoratedTextFieldExample({super.key});
@override
State<DecoratedTextFieldExample> createState() => _DecoratedTextFieldExampleState();
}
class _DecoratedTextFieldExampleState extends State<DecoratedTextFieldExample> {
// Separate controllers for each text field
final TextEditingController _basicController = TextEditingController();
final TextEditingController _outlinedController = TextEditingController();
final TextEditingController _filledController = TextEditingController();
final TextEditingController _prefixController = TextEditingController();
final TextEditingController _suffixController = TextEditingController();
@override
void dispose() {
// Dispose all controllers
_basicController.dispose();
_outlinedController.dispose();
_filledController.dispose();
_prefixController.dispose();
_suffixController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Decorated TextField'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. Basic decoration with label and hint text
// The label tells the user what to enter, hint gives an example
TextField(
controller: _basicController,
decoration: const InputDecoration(
// Label text appears above the field or inside when empty
labelText: 'Username',
// Hint text appears when the field is empty
hintText: 'Enter your username',
// Helper text provides additional guidance
helperText: 'Username must be unique',
// Border style when not focused
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 2. Outlined border with custom colors
// This shows how to customize colors for different states
TextField(
controller: _outlinedController,
decoration: InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
helperText: 'We\'ll never share your email',
// Customize the border colors for different states
// focusedBorder - when the field is selected
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2),
),
// enabledBorder - when the field is active but not focused
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey[400]!),
),
// errorBorder - when there is a validation error
errorBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
),
),
const SizedBox(height: 16),
// 3. Filled style with background color
// Filled fields are common in modern UIs
TextField(
controller: _filledController,
decoration: InputDecoration(
labelText: 'Search',
hintText: 'Search for something...',
// Fill the background with a color
filled: true,
fillColor: Colors.grey[100],
// Prefix icon - shows an icon before the text
prefixIcon: const Icon(Icons.search),
// Border radius can be customized
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
// No border when filled is true
borderSide: BorderSide.none,
),
),
),
const SizedBox(height: 16),
// 4. With prefix and suffix widgets
// Prefix shows before the text, suffix shows after
TextField(
controller: _prefixController,
decoration: const InputDecoration(
labelText: 'Price',
hintText: '0.00',
prefixIcon: Icon(Icons.attach_money),
prefixText: '\$',
suffixText: 'USD',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
// 5. With suffix and clear button
// This demonstrates a common pattern for search fields
TextField(
controller: _suffixController,
decoration: InputDecoration(
labelText: 'Search',
hintText: 'Search...',
prefixIcon: const Icon(Icons.search),
// Suffix icon - shows a clear button when text is not empty
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
// Clear the text when the button is pressed
_suffixController.clear();
},
),
border: const OutlineInputBorder(),
),
),
],
),
),
);
}
}
What's happening here? - InputDecoration customizes TextField appearance - labelText provides context - hintText shows example input - prefixIcon and suffixIcon add icons - filled adds background color
TextField Types
Different keyboard and input types.
/// TextField with different input types
class TextFieldTypesExample extends StatelessWidget {
TextFieldTypesExample({super.key});
// Controllers for each input type
final TextEditingController _textController = TextEditingController();
final TextEditingController _numberController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _multilineController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TextField Types'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. Text input (default)
// Standard text input for general use
TextField(
controller: _textController,
decoration: const InputDecoration(
labelText: 'Text Input',
hintText: 'Enter text',
border: OutlineInputBorder(),
),
// Default keyboard type
keyboardType: TextInputType.text,
),
const SizedBox(height: 16),
// 2. Number input
// Shows numeric keyboard, useful for ages, quantities, etc.
TextField(
controller: _numberController,
decoration: const InputDecoration(
labelText: 'Number Input',
hintText: 'Enter a number',
border: OutlineInputBorder(),
),
// Shows number keyboard
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
// 3. Email input
// Shows email keyboard with @ and .com shortcuts
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email Input',
hintText: 'Enter your email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
// Shows email keyboard
keyboardType: TextInputType.emailAddress,
// Autocorrect is usually disabled for emails
autocorrect: false,
),
const SizedBox(height: 16),
// 4. Phone input
// Shows phone number keyboard with number pad
TextField(
controller: _phoneController,
decoration: const InputDecoration(
labelText: 'Phone Input',
hintText: 'Enter phone number',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
),
// Shows phone keyboard
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
// 5. Password input
// Hides the text with dots for security
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
hintText: 'Enter password',
border: OutlineInputBorder(),
// Common pattern: show/hide password toggle
suffixIcon: Icon(Icons.visibility),
),
// Hides the text with dots (secure input)
obscureText: true,
// Disable autocorrect for passwords
autocorrect: false,
// Don't enable auto-complete for passwords
enableSuggestions: false,
),
const SizedBox(height: 16),
// 6. Multiline input
// Expands vertically for longer text (notes, descriptions, etc.)
TextField(
controller: _multilineController,
decoration: const InputDecoration(
labelText: 'Multiline Input',
hintText: 'Enter multiple lines of text',
border: OutlineInputBorder(),
),
// Allows multiple lines
maxLines: 4,
// Minimum number of lines
minLines: 2,
// Show a counter for maximum characters
maxLength: 200,
// This adds a character counter below the field
// Note: maxLength must be set for the counter to appear
),
],
),
),
);
}
}
What's happening here? - keyboardType controls keyboard appearance - obscureText for password fields - maxLines and minLines for multiline - maxLength for character limits
TextField Validation
Validating user input in TextField.
/// TextField with validation
class ValidationTextFieldExample extends StatefulWidget {
const ValidationTextFieldExample({super.key});
@override
State<ValidationTextFieldExample> createState() => _ValidationTextFieldExampleState();
}
class _ValidationTextFieldExampleState extends State<ValidationTextFieldExample> {
// Controllers for form fields
final TextEditingController _nameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
// Form key for validation
// GlobalKey is used to identify and validate the form
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// Track if the form has been submitted
bool _isSubmitted = false;
@override
void dispose() {
// Clean up controllers
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Validation Example'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
// Form widget with key for validation
// The GlobalKey allows us to validate the form from outside
key: _formKey,
child: Column(
children: [
// 1. Name field with validation
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Name',
hintText: 'Enter your name',
border: OutlineInputBorder(),
// Prefix icon for visual enhancement
prefixIcon: Icon(Icons.person),
),
// Validator function called when form is validated
// Returns an error message if validation fails, null if successful
validator: (value) {
// Check if the field is empty
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
// Check minimum length
if (value.length < 2) {
return 'Name must be at least 2 characters';
}
// Check maximum length
if (value.length > 50) {
return 'Name must be less than 50 characters';
}
return null; // Valid
},
// Called when the user submits the form (presses done/enter)
onFieldSubmitted: (value) {
// This is called when the user presses the done/enter key
// Useful for moving to the next field
},
// Called when the text changes (every keystroke)
// Useful for real-time validation
onChanged: (value) {
// You could perform real-time validation here
// Example: update UI based on validity
},
),
const SizedBox(height: 16),
// 2. Email field with validation
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
// Keyboard type for email
keyboardType: TextInputType.emailAddress,
// Auto-correction should be off for emails
autocorrect: false,
// Validator for email
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
// Simple email validation using regex
// Check if the email contains @ and a domain
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
},
),
const SizedBox(height: 16),
// 3. Password field with validation
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
),
// Hide password text
obscureText: true,
// Disable autocorrect for security
autocorrect: false,
// Validator for password
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a password';
}
// Check minimum length for security
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
// Check for at least one number
if (!value.contains(RegExp(r'[0-9]'))) {
return 'Password must contain at least one number';
}
// Check for at least one uppercase letter
if (!value.contains(RegExp(r'[A-Z]'))) {
return 'Password must contain at least one uppercase letter';
}
return null; // Valid
},
),
const SizedBox(height: 24),
// 4. Submit button
// This button validates the entire form
ElevatedButton(
onPressed: () {
// Validate the form
// This calls the validator on each form field
// Returns true if all validators return null
if (_formKey.currentState!.validate()) {
// Form is valid
setState(() {
_isSubmitted = true;
});
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Form submitted successfully!'),
backgroundColor: Colors.green,
),
);
// You can now access the form data
// Example: _nameController.text, _emailController.text, etc.
} else {
// Form is invalid
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please fix the errors above'),
backgroundColor: Colors.red,
),
);
}
},
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
),
child: const Text('Submit'),
),
// Show submission status
if (_isSubmitted) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.green[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Submitted Data:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
Text('Name: ${_nameController.text}'),
Text('Email: ${_emailController.text}'),
Text('Password: ${'*' * _passwordController.text.length}'),
],
),
),
],
],
),
),
),
);
}
}
What's happening here?
- Form widget groups fields for validation
- validator function checks input and returns errors
- GlobalKey
Real-World Examples
Common patterns with TextField.
/// 1. Search bar with debouncing
class SearchBarExample extends StatefulWidget {
const SearchBarExample({super.key});
@override
State<SearchBarExample> createState() => _SearchBarExampleState();
}
class _SearchBarExampleState extends State<SearchBarExample> {
// Controller for the search field
final TextEditingController _searchController = TextEditingController();
// Timer for debouncing
// Debouncing prevents too many searches while typing
Timer? _debounceTimer;
// Current search query
String _searchQuery = '';
List<String> _results = [];
// Sample data to search through
final List<String> _items = [
'Apple', 'Banana', 'Orange', 'Grape', 'Watermelon',
'Strawberry', 'Pineapple', 'Mango', 'Peach', 'Pear',
];
@override
void initState() {
super.initState();
// Add listener for real-time search
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
// Cancel any pending timer
_debounceTimer?.cancel();
_searchController.dispose();
super.dispose();
}
// Called when the search text changes
void _onSearchChanged() {
// Cancel any pending debounce timer
// This ensures we only search after the user stops typing
_debounceTimer?.cancel();
// Set a new timer for 500ms (debounce delay)
// The search will only run after 500ms of no typing
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
setState(() {
_searchQuery = _searchController.text;
_performSearch(_searchQuery);
});
});
}
// Perform the actual search
void _performSearch(String query) {
if (query.isEmpty) {
_results = [];
return;
}
// Filter items that contain the query (case-insensitive)
_results = _items
.where((item) => item.toLowerCase().contains(query.toLowerCase()))
.toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Search Bar'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Search field with clear button
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search...',
prefixIcon: const Icon(Icons.search),
// Show clear button when text is not empty
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
// Clear the search field
_searchController.clear();
},
)
: null,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
filled: true,
fillColor: Colors.grey[50],
),
),
const SizedBox(height: 16),
// Show search results
if (_searchQuery.isNotEmpty) ...[
Text(
'Results for "${_searchQuery}"',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
],
// Display results
Expanded(
child: _results.isEmpty && _searchQuery.isNotEmpty
? const Center(
child: Text(
'No results found',
style: TextStyle(color: Colors.grey),
),
)
: ListView.builder(
itemCount: _results.isEmpty ? _items.length : _results.length,
itemBuilder: (context, index) {
final item = _results.isEmpty
? _items[index]
: _results[index];
return ListTile(
leading: const Icon(Icons.search),
title: Text(item),
// Highlight matching text
subtitle: _searchQuery.isNotEmpty
? Text(
'Matches: $_searchQuery',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
)
: null,
// Show search indicator
trailing: _searchQuery.isNotEmpty &&
_results.contains(item)
? const Icon(Icons.check, color: Colors.green)
: null,
);
},
),
),
],
),
),
);
}
}
/// 2. Login form with error handling
class LoginFormExample extends StatefulWidget {
const LoginFormExample({super.key});
@override
State<LoginFormExample> createState() => _LoginFormExampleState();
}
class _LoginFormExampleState extends State<LoginFormExample> {
// Controllers for login form
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
// Form key for validation
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// Loading state for submit button
bool _isLoading = false;
// Track if password is visible
bool _obscurePassword = true;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
// Simulate login API call
Future<bool> _login(String email, String password) async {
// Simulate network delay
await Future.delayed(const Duration(seconds: 2));
// Simple validation for demo
// In a real app, this would make an API call
return email == 'test@example.com' && password == 'Password123!';
}
// Handle login submission
Future<void> _submitLogin() async {
// Validate the form
if (!_formKey.currentState!.validate()) {
return; // Form is invalid
}
// Show loading state
setState(() {
_isLoading = true;
});
try {
// Attempt login
final success = await _login(
_emailController.text,
_passwordController.text,
);
if (success) {
// Login successful
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login successful!'),
backgroundColor: Colors.green,
),
);
// Navigate to home screen
} else {
// Login failed
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid email or password'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
// Handle error
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
} finally {
// Hide loading state
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// App logo/icon
const Icon(
Icons.lock_outline,
size: 80,
color: Colors.blue,
),
const SizedBox(height: 32),
// Email field
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
keyboardType: TextInputType.emailAddress,
autocorrect: false,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!value.contains('@')) {
return 'Please enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
// Password field
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
prefixIcon: const Icon(Icons.lock),
// Show/hide password toggle
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
// Toggle password visibility
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
obscureText: _obscurePassword,
autocorrect: false,
validator: (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;
},
),
const SizedBox(height: 24),
// Login button with loading state
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _isLoading ? null : _submitLogin,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: const Text(
'Login',
style: TextStyle(fontSize: 16),
),
),
),
const SizedBox(height: 16),
// Forgot password link
TextButton(
onPressed: () {
// Navigate to forgot password
},
child: const Text('Forgot Password?'),
),
// Demo credentials hint
Container(
margin: const EdgeInsets.only(top: 32),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue[100]!),
),
child: Column(
children: [
const Text(
'Demo Credentials:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text('Email: test@example.com'),
Text('Password: Password123!'),
],
),
),
],
),
),
),
);
}
}
What's happening here? - Search with debouncing to reduce calls - Real-time filtering results - Login form with validation - Loading state on submit - Show/hide password toggle
Best Practices
Use Controllers Properly
// Good - Controllers in State
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
// Bad - Creating controllers in build
@override
Widget build(BuildContext context) {
final controller = TextEditingController(); // Created every rebuild
return TextField(controller: controller);
}
Use Form for Validation
// Good - Form with validation
Form(
key: _formKey,
child: TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'Required';
}
return null;
},
),
)
// Bad - Manual validation
TextField(
onChanged: (value) {
if (value.isEmpty) {
// Manual error handling
}
},
)
Use InputDecoration for Styling
// Good - Consistent styling
InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
)
// Bad - Inconsistent styling
TextField(
decoration: InputDecoration(labelText: 'Email'),
)
Common Mistakes
Not Disposing Controllers
Wrong:
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final TextEditingController _controller = TextEditingController();
// No dispose - memory leak
}
Correct:
class _MyWidgetState extends State<MyWidget> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Not Using keyboardType
Wrong:
// Shows text keyboard for numbers
TextField(
// Missing keyboardType
decoration: InputDecoration(labelText: 'Age'),
)
Correct:
// Shows number keyboard
TextField(
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: 'Age'),
)
Summary
TextField is a versatile widget for collecting user input. Use TextEditingController to manage text state, InputDecoration for styling, and Form for validation. Support different input types with keyboardType and handle user interactions with onChanged and onSubmitted.
Next Steps
Did You Know?
- TextEditingController must be disposed
- InputDecoration provides extensive styling
- keyboardType controls keyboard appearance
- Form validates multiple fields
- TextField supports multiline input
- TextField can be password-protected
- InputFormatter can restrict input
- TextField supports focus management