Keyboard Handling
Understand how to handle keyboard input and behavior in Flutter applications.
What is it?
Keyboard handling refers to the management of keyboard input, appearance, dismissal, and behavior in Flutter applications. It covers everything from showing and hiding the keyboard, handling keyboard events, managing keyboard types, and controlling how the keyboard interacts with your UI. Proper keyboard handling is essential for creating a smooth user experience, especially on mobile devices.
Why does it exist?
Keyboard handling exists to:
- Control keyboard appearance and behavior
- Handle keyboard input events
- Manage keyboard dismissal
- Support different input types
- Handle keyboard shortcuts (desktop)
- Manage keyboard focus
- Provide keyboard navigation
Keyboard Types
Different keyboard types for different inputs.
// Import required packages
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Keyboard types example
class KeyboardTypesExample extends StatefulWidget {
const KeyboardTypesExample({super.key});
@override
State<KeyboardTypesExample> createState() => _KeyboardTypesExampleState();
}
class _KeyboardTypesExampleState extends State<KeyboardTypesExample> {
// Controllers for each field
final TextEditingController _textController = TextEditingController();
final TextEditingController _numberController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _urlController = TextEditingController();
final TextEditingController _datetimeController = TextEditingController();
@override
void dispose() {
_textController.dispose();
_numberController.dispose();
_emailController.dispose();
_phoneController.dispose();
_urlController.dispose();
_datetimeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Keyboard Types'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. Text input (default)
TextField(
controller: _textController,
decoration: const InputDecoration(
labelText: 'Text Input',
hintText: 'Regular text keyboard',
border: OutlineInputBorder(),
),
// Default keyboard - shows standard keyboard
keyboardType: TextInputType.text,
),
const SizedBox(height: 16),
// 2. Number input
// Shows numeric keyboard with digits
TextField(
controller: _numberController,
decoration: const InputDecoration(
labelText: 'Number Input',
hintText: 'Numeric keyboard',
border: OutlineInputBorder(),
),
// Shows number keyboard with digits
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
// 3. Email input
// Shows email-specific keyboard with @ and .com
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email Input',
hintText: 'Email keyboard with @',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
// Disable autocorrect for emails
autocorrect: false,
),
const SizedBox(height: 16),
// 4. Phone input
// Shows phone number keyboard
TextField(
controller: _phoneController,
decoration: const InputDecoration(
labelText: 'Phone Input',
hintText: 'Phone number keyboard',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
// 5. URL input
// Shows URL-specific keyboard with . and /
TextField(
controller: _urlController,
decoration: const InputDecoration(
labelText: 'URL Input',
hintText: 'URL keyboard with / and .',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.url,
autocorrect: false,
),
const SizedBox(height: 16),
// 6. DateTime input
TextField(
controller: _datetimeController,
decoration: const InputDecoration(
labelText: 'Date/Time Input',
hintText: 'Date/Time keyboard',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.datetime,
),
],
),
),
);
}
}
What's happening here? - Text: Standard keyboard - Number: Numeric keypad - Email: Email-specific keyboard - Phone: Phone number keyboard - URL: URL-specific keyboard - DateTime: DateTime keyboard
Keyboard Actions
Controlling keyboard actions and buttons.
/// Keyboard actions example
class KeyboardActionsExample extends StatefulWidget {
const KeyboardActionsExample({super.key});
@override
State<KeyboardActionsExample> createState() => _KeyboardActionsExampleState();
}
class _KeyboardActionsExampleState extends State<KeyboardActionsExample> {
// Controllers for form fields
final TextEditingController _field1Controller = TextEditingController();
final TextEditingController _field2Controller = TextEditingController();
final TextEditingController _field3Controller = TextEditingController();
final TextEditingController _field4Controller = TextEditingController();
// Focus nodes for navigation
final FocusNode _field1Focus = FocusNode();
final FocusNode _field2Focus = FocusNode();
final FocusNode _field3Focus = FocusNode();
final FocusNode _field4Focus = FocusNode();
@override
void dispose() {
_field1Controller.dispose();
_field2Controller.dispose();
_field3Controller.dispose();
_field4Controller.dispose();
_field1Focus.dispose();
_field2Focus.dispose();
_field3Focus.dispose();
_field4Focus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Keyboard Actions'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. Text input with Done button
// textInputAction controls the action button on the keyboard
TextField(
controller: _field1Controller,
focusNode: _field1Focus,
decoration: const InputDecoration(
labelText: 'Field 1 - Done',
hintText: 'Press Done to submit',
border: OutlineInputBorder(),
),
// Done button: submits the form
textInputAction: TextInputAction.done,
onSubmitted: (value) {
// Called when Done is pressed
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Submitted!'),
backgroundColor: Colors.green,
),
);
// Dismiss keyboard
_field1Focus.unfocus();
},
),
const SizedBox(height: 16),
// 2. Text input with Next button
TextField(
controller: _field2Controller,
focusNode: _field2Focus,
decoration: const InputDecoration(
labelText: 'Field 2 - Next',
hintText: 'Press Next to go to next field',
border: OutlineInputBorder(),
),
// Next button: moves to next field
textInputAction: TextInputAction.next,
onSubmitted: (value) {
// Move focus to next field
_field3Focus.requestFocus();
},
),
const SizedBox(height: 16),
// 3. Text input with Continue button
TextField(
controller: _field3Controller,
focusNode: _field3Focus,
decoration: const InputDecoration(
labelText: 'Field 3 - Continue',
hintText: 'Press Continue to continue',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.continueAction,
onSubmitted: (value) {
// Move to next field
_field4Focus.requestFocus();
},
),
const SizedBox(height: 16),
// 4. Text input with Send button
TextField(
controller: _field4Controller,
focusNode: _field4Focus,
decoration: const InputDecoration(
labelText: 'Field 4 - Send',
hintText: 'Press Send to send message',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.send,
onSubmitted: (value) {
// Send the message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Sending: $value'),
backgroundColor: Colors.blue,
),
);
_field4Focus.unfocus();
},
),
const SizedBox(height: 24),
// Info about keyboard actions
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue[200]!),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Keyboard Actions:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(height: 8),
Text('• Done: Submit/Complete form'),
Text('• Next: Move to next field'),
Text('• Continue: Continue action'),
Text('• Send: Send message'),
Text('• Search: Perform search'),
Text('• Go: Navigate'),
],
),
),
],
),
),
);
}
}
What's happening here? - textInputAction controls keyboard button - Done: Submits form - Next: Moves to next field - Continue: Continues action - Send: Sends message
Keyboard Dismissal
Showing and hiding the keyboard.
/// Keyboard dismissal example
class KeyboardDismissalExample extends StatefulWidget {
const KeyboardDismissalExample({super.key});
@override
State<KeyboardDismissalExample> createState() => _KeyboardDismissalExampleState();
}
class _KeyboardDismissalExampleState extends State<KeyboardDismissalExample> {
// Controllers for fields
final TextEditingController _controller1 = TextEditingController();
final TextEditingController _controller2 = TextEditingController();
// Focus nodes
final FocusNode _focusNode1 = FocusNode();
final FocusNode _focusNode2 = FocusNode();
@override
void dispose() {
_controller1.dispose();
_controller2.dispose();
_focusNode1.dispose();
_focusNode2.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Keyboard Dismissal'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. Dismiss on submit
TextField(
controller: _controller1,
focusNode: _focusNode1,
decoration: const InputDecoration(
labelText: 'Dismiss on Submit',
hintText: 'Press Done to dismiss',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.done,
onSubmitted: (value) {
// 1. Unfocus to dismiss keyboard
_focusNode1.unfocus();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Keyboard dismissed'),
duration: Duration(milliseconds: 500),
),
);
},
),
const SizedBox(height: 16),
// 2. Tap outside to dismiss
// GestureDetector to detect taps outside text fields
GestureDetector(
onTap: () {
// 2. Unfocus all fields when tapping outside
// This dismisses the keyboard
FocusScope.of(context).unfocus();
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
TextField(
controller: _controller2,
focusNode: _focusNode2,
decoration: const InputDecoration(
labelText: 'Tap outside to dismiss',
hintText: 'Tap on the background',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
const Text(
'Tap anywhere outside the text field to dismiss keyboard',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
),
const SizedBox(height: 24),
// 3. Manual dismiss buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Dismiss keyboard
ElevatedButton(
onPressed: () {
// 3. Use FocusScope to dismiss all
FocusScope.of(context).unfocus();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Keyboard dismissed'),
duration: Duration(milliseconds: 500),
),
);
},
child: const Text('Dismiss Keyboard'),
),
const SizedBox(width: 8),
// Focus field 1
ElevatedButton(
onPressed: () {
_focusNode1.requestFocus();
},
child: const Text('Focus Field 1'),
),
],
),
const SizedBox(height: 16),
// 4. Resize view when keyboard appears
// Using SingleChildScrollView automatically handles this
const Text(
'Note: Using SingleChildScrollView helps with keyboard resizing',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
);
}
}
What's happening here? - unfocus() dismisses keyboard - FocusScope.unfocus() dismisses all - GestureDetector detects outside taps - SingleChildScrollView handles resizing
Keyboard Shortcuts
Handling keyboard shortcuts (desktop/web).
/// Keyboard shortcuts example
class KeyboardShortcutsExample extends StatefulWidget {
const KeyboardShortcutsExample({super.key});
@override
State<KeyboardShortcutsExample> createState() => _KeyboardShortcutsExampleState();
}
class _KeyboardShortcutsExampleState extends State<KeyboardShortcutsExample> {
// Controllers for text fields
final TextEditingController _controller = TextEditingController();
String _shortcutMessage = 'Press a shortcut key...';
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Keyboard Shortcuts'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. SingleShortcut widget for keyboard shortcuts
// This detects specific key combinations
CallbackShortcuts(
bindings: {
// Ctrl+S to save
const SingleActivator(LogicalKeyboardKey.keyS, control: true): () {
setState(() {
_shortcutMessage = 'Ctrl+S pressed - Save action';
});
},
// Ctrl+Z to undo
const SingleActivator(LogicalKeyboardKey.keyZ, control: true): () {
setState(() {
_shortcutMessage = 'Ctrl+Z pressed - Undo action';
});
},
// Ctrl+Shift+Z to redo
const SingleActivator(
LogicalKeyboardKey.keyZ,
control: true,
shift: true,
): () {
setState(() {
_shortcutMessage = 'Ctrl+Shift+Z pressed - Redo action';
});
},
// Escape to cancel
const SingleActivator(LogicalKeyboardKey.escape): () {
setState(() {
_shortcutMessage = 'Escape pressed - Cancel action';
});
},
},
child: Focus(
autofocus: true,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue[200]!),
),
child: Column(
children: [
const Text(
'Focus here and try shortcuts:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
_shortcutMessage,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 8),
const Text(
'Ctrl+S, Ctrl+Z, Ctrl+Shift+Z, Escape',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
),
),
const SizedBox(height: 24),
// 2. Text field with keyboard shortcuts
TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'Try shortcuts in text field',
hintText: 'Type something...',
border: OutlineInputBorder(),
helperText: 'Ctrl+A to select all, Ctrl+C to copy',
),
),
const SizedBox(height: 16),
// 3. Shortcut info
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Common Shortcuts:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(height: 8),
Text('• Ctrl+A: Select all'),
Text('• Ctrl+C: Copy'),
Text('• Ctrl+V: Paste'),
Text('• Ctrl+X: Cut'),
Text('• Ctrl+Z: Undo'),
Text('• Ctrl+S: Save'),
Text('• Escape: Cancel'),
],
),
),
],
),
),
);
}
}
What's happening here? - CallbackShortcuts for key bindings - SingleActivator defines key combinations - Focus required for shortcut detection - Desktop/web keyboard shortcuts
Real-World Examples
Common keyboard handling patterns.
/// 1. Search field with keyboard handling
class SearchFieldWithKeyboard extends StatefulWidget {
const SearchFieldWithKeyboard({super.key});
@override
State<SearchFieldWithKeyboard> createState() => _SearchFieldWithKeyboardState();
}
class _SearchFieldWithKeyboardState extends State<SearchFieldWithKeyboard> {
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocus = FocusNode();
List<String> _results = [];
final List<String> _allItems = [
'Apple', 'Banana', 'Orange', 'Grape', 'Watermelon',
'Strawberry', 'Pineapple', 'Mango', 'Peach', 'Pear',
];
@override
void dispose() {
_searchController.dispose();
_searchFocus.dispose();
super.dispose();
}
void _performSearch(String query) {
setState(() {
if (query.isEmpty) {
_results = [];
} else {
_results = _allItems
.where((item) => item
.toLowerCase()
.contains(query.toLowerCase()))
.toList();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Search with Keyboard'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Search field with keyboard handling
TextField(
controller: _searchController,
focusNode: _searchFocus,
decoration: InputDecoration(
hintText: 'Search...',
prefixIcon: const Icon(Icons.search),
// Clear button
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_performSearch('');
},
)
: null,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
filled: true,
fillColor: Colors.grey[50],
),
// Search action on keyboard
textInputAction: TextInputAction.search,
// Trigger search when submitted
onSubmitted: _performSearch,
// Real-time search as user types
onChanged: _performSearch,
),
const SizedBox(height: 16),
// Show results or empty state
Expanded(
child: _results.isEmpty
? Center(
child: Text(
_searchController.text.isEmpty
? 'Start typing to search'
: 'No results found',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
)
: ListView.builder(
itemCount: _results.length,
itemBuilder: (context, index) {
return ListTile(
leading: const Icon(Icons.search),
title: Text(_results[index]),
);
},
),
),
],
),
),
);
}
}
/// 2. Form with keyboard navigation
class KeyboardNavigationForm extends StatefulWidget {
const KeyboardNavigationForm({super.key});
@override
State<KeyboardNavigationForm> createState() => _KeyboardNavigationFormState();
}
class _KeyboardNavigationFormState extends State<KeyboardNavigationForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// Controllers
final TextEditingController _firstNameController = TextEditingController();
final TextEditingController _lastNameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
// Focus nodes for navigation
final FocusNode _firstNameFocus = FocusNode();
final FocusNode _lastNameFocus = FocusNode();
final FocusNode _emailFocus = FocusNode();
final FocusNode _passwordFocus = FocusNode();
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_firstNameFocus.dispose();
_lastNameFocus.dispose();
_emailFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Keyboard Navigation'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
// First name
TextFormField(
controller: _firstNameController,
focusNode: _firstNameFocus,
decoration: const InputDecoration(
labelText: 'First Name',
hintText: 'Enter first name',
border: OutlineInputBorder(),
),
// Next button to move to last name
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_lastNameFocus.requestFocus();
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'First name required';
}
return null;
},
),
const SizedBox(height: 16),
// Last name
TextFormField(
controller: _lastNameController,
focusNode: _lastNameFocus,
decoration: const InputDecoration(
labelText: 'Last Name',
hintText: 'Enter last name',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_emailFocus.requestFocus();
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Last name required';
}
return null;
},
),
const SizedBox(height: 16),
// Email
TextFormField(
controller: _emailController,
focusNode: _emailFocus,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'Enter email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_passwordFocus.requestFocus();
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email required';
}
return null;
},
),
const SizedBox(height: 16),
// Password
TextFormField(
controller: _passwordController,
focusNode: _passwordFocus,
decoration: const InputDecoration(
labelText: 'Password',
hintText: 'Enter password',
border: OutlineInputBorder(),
),
obscureText: true,
// Done button to submit
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) {
// Submit form when done is pressed
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Form submitted!'),
backgroundColor: Colors.green,
),
);
// Dismiss keyboard
FocusScope.of(context).unfocus();
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password required';
}
return null;
},
),
const SizedBox(height: 24),
// Submit button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Form is valid
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Form submitted!'),
backgroundColor: Colors.green,
),
);
FocusScope.of(context).unfocus();
}
},
child: const Text('Submit'),
),
),
],
),
),
),
);
}
}
What's happening here? - Search with keyboard actions - Form with keyboard navigation - Next/Done buttons for form fields - Keyboard focus traversal
Best Practices
Use Appropriate Keyboard Type
// Good - Correct keyboard type
TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(labelText: 'Email'),
)
// Bad - Wrong keyboard type
TextField(
keyboardType: TextInputType.text,
decoration: InputDecoration(labelText: 'Email'),
)
Handle Keyboard Dismissal
// Good - Dismiss keyboard when done
onFieldSubmitted: (value) {
FocusScope.of(context).unfocus();
}
// Bad - Keep keyboard open
onFieldSubmitted: (value) {
// Keyboard stays open
}
Use textInputAction for Navigation
// Good - Proper navigation
TextField(
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) => nextFocus.requestFocus(),
)
// Bad - No navigation
TextField(
// No textInputAction
// User has to tap the next field manually
)
Common Mistakes
Not Dismissing Keyboard
Wrong:
// Keyboard stays open after submit
onSubmitted: (value) {
submitForm();
}
Correct:
// Dismiss keyboard after submit
onSubmitted: (value) {
submitForm();
FocusScope.of(context).unfocus();
}
Wrong Keyboard Type
Wrong:
// Shows text keyboard for phone number
TextField(
keyboardType: TextInputType.text,
decoration: InputDecoration(labelText: 'Phone'),
)
Correct:
// Shows phone keyboard
TextField(
keyboardType: TextInputType.phone,
decoration: InputDecoration(labelText: 'Phone'),
)
Summary
Keyboard handling controls keyboard appearance, behavior, and dismissal. Use keyboardType for appropriate keyboards, textInputAction for button actions, and unfocus() for dismissal. Proper keyboard handling improves user experience, especially on mobile devices.
Next Steps
Did You Know?
- keyboardType controls keyboard appearance
- textInputAction controls keyboard button
- unfocus() dismisses keyboard
- FocusScope manages focus groups
- CallbackShortcuts handle keyboard shortcuts
- SingleChildScrollView handles keyboard resizing
- GestureDetector can detect outside taps
- Keyboard shortcuts work on desktop/web