Handling user input
Now that you know how to manage state in your Flutter app, how can you let users interact with your app and change its state?
Introduction to handling user input
#As a multi-platform UI framework, there are many different ways for users to interact with a Flutter app. The resources in this section introduce you to some of the common widgets used for enabling user interaction within your app.
Some user input mechanisms, like scrolling, have already been covered in Layouts.
Reference: The widget catalog has an inventory of commonly used widgets in the Material and Cupertino libraries.
Next, we'll cover a few of the Material widgets that support common use cases for handling user input in your Flutter app.
Buttons
#
Buttons allow a user to initiate an action in the UI by clicking or tapping. The Material library provides a variety of button types that are functionally similar, but styled differently for various use cases, including:
-
ElevatedButton: A button with some depth. Use elevated buttons to add dimension to otherwise mostly flat layouts. -
FilledButton: A filled button that should be used for important, final actions that complete a flow, like Save, Join now, or Confirm. -
Tonal Button: A middle ground button betweenFilledButtonandOutlinedButton. They're useful in contexts where a lower-priority button requires more emphasis than an outline, like Next. -
OutlinedButton: A button with text and a visible border. These buttons contain actions that are important, but aren't the primary action in an app. -
TextButton: Clickable text, without a border. Since text buttons don't have visible borders, they must rely on their position relative to other content for context. IconButton: A button with an icon.-
FloatingActionButton: An icon button that hovers over content to promote a primary action.
There are usually 3 main aspects to constructing a button: style, callback, and its child, as seen in the following ElevatedButton sample code:
-
A button's callback function,
onPressed, determines what happens when the button is clicked, therefore, this function is where you update your app state. If the callback isnull, the button is disabled and nothing happens when a user presses the button. -
The button's
child, which is displayed within the button's content area, is usually text or an icon that indicates the button's purpose. Finally, a button's
stylecontrols its appearance: color, border, and so on.
int count = 0; @override Widget build(BuildContext context) { return ElevatedButton( style: ElevatedButton.styleFrom( textStyle: const TextStyle(fontSize: 20), ), onPressed: () { setState(() { count += 1; }); }, child: const Text('Enabled'), ); }
Checkpoint: Complete this tutorial that teaches you how to build a "favorite" button: Add interactivity to your Flutter app
API Docs: ElevatedButton • FilledButton • OutlinedButton • TextButton • IconButton • FloatingActionButton
Text
#Several widgets support text input.
SelectableText
# Flutter's Text widget displays text on the screen, but doesn't allow users to highlight or copy the text. SelectableText displays a string of user-selectable text.
@override Widget build(BuildContext context) { return const SelectableText(''' Two households, both alike in dignity, In fair Verona, where we lay our scene, From ancient grudge break to new mutiny, Where civil blood makes civil hands unclean. From forth the fatal loins of these two foes'''); }
RichText
# RichText lets you display strings of rich text in your app. TextSpan, similar to RichText, allows you to display parts of text with different text styles. It's not for handling user input, but is useful if you're allowing users edit and format text.
@override Widget build(BuildContext context) { return RichText( text: TextSpan( text: 'Hello ', style: DefaultTextStyle.of(context).style, children: const <TextSpan>[ TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: ' world!'), ], ), ); }
Code: Rich Text Editor code
TextField
# A TextField lets users enter text in text box using a hardware or onscreen keyboard.
TextFields have many different properties and configurations. A few of the highlights:
-
InputDecorationdetermines the text field's appearance, such as color and border. -
controller: ATextEditingControllercontrols the text being edited. Why might you need a controller? By default, your app's users can type into the text field, but if you want to programmatically control theTextFieldand clear its value, for example, you'll need aTextEditingController. -
onChanged: This callback function triggers when the user changes the text field's value, such as when inserting or removing text. -
onSubmitted: This callback is triggered when the user indicates that they are done editing the text in the field; for example, by tapping the "enter" key when the text field is in focus.
The class supports other configurable properties, such as obscureText that turns each letter into a readOnly circle as its entered and readOnly which prevents the user from changing the text.
final TextEditingController _controller = TextEditingController(); @override Widget build(BuildContext context) { return TextField( controller: _controller, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Mascot Name', ), ); }
Checkpoint: Complete this 4-part cookbook series that walks you through how to create a text field, retrieve its value, and update your app state:
Form
# Form is an optional container for grouping together multiple form field widgets, such as TextField.
Each individual form field should be wrapped in a FormField widget with the Form widget as a common ancestor. Convenience widgets exist that pre-wrap form field widgets in a FormField for you. For example, the Form widget version of TextField is TextFormField.
Using a Form provides access to a FormState, which lets you save, reset, and validate each FormField that descends from this Form. You can also provide a GlobalKey to identify a specific form, as shown in the following code:
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ TextFormField( decoration: const InputDecoration( hintText: 'Enter your email', ), validator: (String? value) { if (value == null || value.isEmpty) { return 'Please enter some text'; } return null; }, ), Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: ElevatedButton( onPressed: () { // Validate returns true if the form is valid, or false otherwise. if (_formKey.currentState!.validate()) { // Process data. } }, child: const Text('Submit'), ), ), ], ), ); } Checkpoint: Complete this tutorial to learn how to build a form with validation.
Demo: Form app
Code: Form app code
API Docs: TextField • RichText • SelectableText • Form
Select a value from a group of options
#Provide a way to users to select from several options.
SegmentedButton
# SegmentedButton allows users to select from a minimal group of 2-5 items.
The data type, <T>, can be a built-in type such as int, String, bool or an enum. A SegmentedButton has a few relevant properties:
-
segments, a list ofButtonSegments, where each represents a "segment" or option that the user can select. Visually, eachButtonSegmentcan have an icon, text label, or both. -
multiSelectionEnabledindicates whether the user is allowed to select multiple options. This property defaults to false. -
selectedidentifies the currently selected value(s). Note:selectedis of type ofSet<T>, so if you're only allowing users to select one value, that value must be provided as aSetwith a single element. -
The
onSelectionChangedcallback triggers when a user selects any segments. It provides a list of the selected segments so you can update your app state. -
Additional styling parameters allow you to modify the button's appearance. For example,
styletakes aButtonStyle, providing a way to configure aselectedIcon.
enum Calendar { day, week, month, year } // StatefulWidget... Calendar calendarView = Calendar.day; @override Widget build(BuildContext context) { return SegmentedButton<Calendar>( segments: const <ButtonSegment<Calendar>>[ ButtonSegment<Calendar>( value: Calendar.day, label: Text('Day'), icon: Icon(Icons.calendar_view_day)), ButtonSegment<Calendar>( value: Calendar.week, label: Text('Week'), icon: Icon(Icons.calendar_view_week)), ButtonSegment<Calendar>( value: Calendar.month, label: Text('Month'), icon: Icon(Icons.calendar_view_month)), ButtonSegment<Calendar>( value: Calendar.year, label: Text('Year'), icon: Icon(Icons.calendar_today)), ], selected: <Calendar>{calendarView}, onSelectionChanged: (Set<Calendar> newSelection) { setState(() { Suggested change // By default there is only a single segment that can be // selected at one time, so its value is always the first // By default, only a single segment can be // selected at one time, so its value is always the first calendarView = newSelection.first; }); }, ); }
Chip
# Chip is a compact way of representing an attribute, text, entity, or action for a specific context. Specialized Chip widgets exist for specific use cases:
- InputChip represents a complex piece of information, such as an entity (person, place, or thing), or conversational text, in a compact form.
- ChoiceChip allows a single selection from a set of options. Choice chips contain related descriptive text or categories.
- FilterChip uses tags or descriptive words to filter content.
- ActionChip represents an action related to primary content.
Every Chip widget requires a label. It can optionally have an avatar (such as an icon or a user's profile picture) and an onDeleted callback, which shows a delete icon that when triggered, deletes the chip. A Chip widget's appearance can also be customized by setting a number of optional parameters such as shape, color, and iconTheme.
You will typically use Wrap, a widget that displays its children in multiple horizontal or vertical runs, to make sure your chips wrap and don't get cut off at the edge of your app.
@override Widget build(BuildContext context) { return const SizedBox( width: 500, child: Wrap( alignment: WrapAlignment.center, spacing: 8, runSpacing: 4, children: [ Chip( avatar: CircleAvatar( backgroundImage: AssetImage('assets/images/dash_chef.png')), label: Text('Chef Dash'), ), Chip( avatar: CircleAvatar( backgroundImage: AssetImage('assets/images/dash_firefighter.png')), label: Text('Firefighter Dash'), ), Chip( avatar: CircleAvatar( backgroundImage: AssetImage('assets/images/dash_musician.png')), label: Text('Musician Dash'), ), Chip( avatar: CircleAvatar( backgroundImage: AssetImage('assets/images/dash_artist.png')), label: Text('Artist Dash'), ), ], ), ); }
DropdownMenu
# A DropdownMenu allows users to select a choice from a menu of options and places the selected text into a TextField. It also allows users to filter the menu items based on the text input.
Configuration parameters include the following:
-
dropdownMenuEntriesprovides a list ofDropdownMenuEntrys that describes each menu item. The menu might contain information such as a text label, and a leading or trailing icon. (This is also the only required parameter.) TextEditingControllerallows programmatically controlling theTextField.- The
onSelectedcallback triggers when the user selects an option. initialSelectionallows you to configure the default value.- Additional parameters are also available for customizing the widget's look and behavior.
enum ColorLabel { blue('Blue', Colors.blue), pink('Pink', Colors.pink), green('Green', Colors.green), orange('Orange', Colors.orange), grey('Grey', Colors.grey); const ColorLabel(this.label, this.color); final String label; final Color color; } // StatefulWidget... @override Widget build(BuildContext context) { return DropdownMenu<ColorLabel>( initialSelection: ColorLabel.green, controller: colorController, // requestFocusOnTap is enabled/disabled by platforms when it is null. // On mobile platforms, this is false by default. Setting this to true will // trigger focus request on the text field and virtual keyboard will appear // afterward. On desktop platforms however, this defaults to true. requestFocusOnTap: true, label: const Text('Color'), onSelected: (ColorLabel? color) { setState(() { selectedColor = color; }); }, dropdownMenuEntries: ColorLabel.values .map<DropdownMenuEntry<ColorLabel>>( (ColorLabel color) { return DropdownMenuEntry<ColorLabel>( value: color, label: color.label, enabled: color.label != 'Grey', style: MenuItemButton.styleFrom( foregroundColor: color.color, ), ); }).toList(), ); }
Slider
# The Slider widget lets a user adjust a value by moving an indicator, such as a volume bar.
Configuration parameters for the Slider widget:
valuerepresents the slider's current valueonChangedis the callback that gets triggered when the handle is movedminandmaxestablish minimum and maximum values allowed by the slider-
divisionsestablishes a discrete interval with which the user can move the handle along the track.
double _currentVolume = 1; @override Widget build(BuildContext context) { return Slider( value: _currentVolume, max: 5, divisions: 5, label: _currentVolume.toString(), onChanged: (double value) { setState(() { _currentVolume = value; }); }, ); }
Video: Slider, RangeSlider, CupertinoSlider (Widget of the Week)
API Docs: SegmentedButton • DropdownMenu • Slider • Chip
Toggle between values
#There are several ways that your UI can allow toggling between values.
Checkbox, Switch, and Radio
# Provide an option to toggle a single value on and off. The functional logic behind these widgets are the same, as all 3 are built on top of ToggleableStateMixin, though each provides slight presentation differences.:
-
Checkboxis a container that is empty when false or filled with a checkmark when true. -
Switchhas a handle that is on the left when false and slides to the right when true. -
Radiois similar to aCheckboxin that it's a container that is empty when false, but filled in when true.
The configuration for Checkbox and Switch contain:
- a
valuethat istrueorfalse - and an
onChangedcallback which is triggered when the user toggles the widget
Checkbox
#bool isChecked = false; @override Widget build(BuildContext context) { return Checkbox( checkColor: Colors.white, value: isChecked, onChanged: (bool? value) { setState(() { isChecked = value!; }); }, ); }
Switch
#bool light = true; @override Widget build(BuildContext context) { return Switch( // This bool value toggles the switch. value: light, activeColor: Colors.red, onChanged: (bool value) { // This is called when the user toggles the switch. setState(() { light = value; }); }, ); }
Radio
# A group of Radio buttons that allows the user to select between mutually exclusive values. When the user selects a radio button in a group, the other radio buttons are unselected.
- A particular
Radiobutton'svaluerepresent that button's value, - The selected value for a group of radio buttons is identified by the
groupValueparameter. -
Radioalso has anonChangedcallback that gets triggered when users click it, likeSwitchandCheckbox
enum Character { musician, chef, firefighter, artist } class RadioExample extends StatefulWidget { const RadioExample({super.key}); @override State<RadioExample> createState() => _RadioExampleState(); } class _RadioExampleState extends State<RadioExample> { Character? _character = Character.musician; void setCharacter(Character? value) { setState(() { _character = value; }); } @override Widget build(BuildContext context) { return Column( children: <Widget>[ ListTile( title: const Text('Musician'), leading: Radio<Character>( value: Character.musician, groupValue: _character, onChanged: setCharacter, ), ), ListTile( title: const Text('Chef'), leading: Radio<Character>( value: Character.chef, groupValue: _character, onChanged: setCharacter, ), ), ListTile( title: const Text('Firefighter'), leading: Radio<Character>( value: Character.firefighter, groupValue: _character, onChanged: setCharacter, ), ), ListTile( title: const Text('Artist'), leading: Radio<Character>( value: Character.artist, groupValue: _character, onChanged: setCharacter, ), ), ], ); } }
Bonus: CheckboxListTile & SwitchListTile
# These convenience widgets are the same checkbox and switch widgets, but support a label (as a ListTile).
double timeDilation = 1.0; bool _lights = false; @override Widget build(BuildContext context) { return Column( children: [ CheckboxListTile( title: const Text('Animate Slowly'), value: timeDilation != 1.0, onChanged: (bool? value) { setState(() { timeDilation = value! ? 10.0 : 1.0; }); }, secondary: const Icon(Icons.hourglass_empty), ), SwitchListTile( title: const Text('Lights'), value: _lights, onChanged: (bool value) { setState(() { _lights = value; }); }, secondary: const Icon(Icons.lightbulb_outline), ), ], ); }
API Docs: Checkbox • CheckboxListTile • Switch • SwitchListTile • Radio
Select a date or time
#Widgets are provided so the user can select a date and time.
There is a set of dialogs that enable users to select a date or time, as you'll see in the following sections. With the exception of differing date types - DateTime for dates vs TimeOfDay for time - these dialogs function similarly, you can configure them by providing:
- a default
initialDateorinitialTime - or an
initialEntryModethat determines the picker UI that's displayed.
DatePickerDialog
# This dialog allows the user to select a date or a range of dates. Activate by calling the showDatePicker function, which returns a Future<DateTime>, so don't forget to await the asynchronous function call!
DateTime? selectedDate; @override Widget build(BuildContext context) { var date = selectedDate; return Column(children: [ Text( date == null ? 'You haven't picked a date yet.' : DateFormat('MM-dd-yyyy').format(date), ), ElevatedButton.icon( icon: const Icon(Icons.calendar_today), onPressed: () async { var pickedDate = await showDatePicker( context: context, initialEntryMode: DatePickerEntryMode.calendarOnly, initialDate: DateTime.now(), firstDate: DateTime(2019), lastDate: DateTime(2050), ); setState(() { selectedDate = pickedDate; }); }, label: const Text('Pick a date'), ) ]); }
TimePickerDialog
# TimePickerDialog is a dialog that presents a time picker. It can be activated by calling the showTimePicker() function. Instead of returning a Future<DateTime>, showTimePicker instead returns a Future<TimeOfDay>. Once again, don't forget to await the function call!
TimeOfDay? selectedTime; @override Widget build(BuildContext context) { var time = selectedTime; return Column(children: [ Text( time == null ? 'You haven't picked a time yet.' : time.format(context), ), ElevatedButton.icon( icon: const Icon(Icons.calendar_today), onPressed: () async { var pickedTime = await showTimePicker( context: context, initialEntryMode: TimePickerEntryMode.dial, initialTime: TimeOfDay.now(), ); setState(() { selectedTime = pickedTime; }); }, label: const Text('Pick a time'), ) ]); }
API Docs: showDatePicker • showTimePicker
Swipe & slide
# A Dismissible is a widget that enables users to dismiss it by swiping. It has a number of configuration parameters, including:
- A
childwidget - An
onDismissedcallback that is triggered when the user swipes - Styling parameters such as
background - It's important to include a
keyobject as well so that they can be uniquely identified from siblingDismissiblewidgets in the widget tree.
List<int> items = List<int>.generate(100, (int index) => index); @override Widget build(BuildContext context) { return ListView.builder( itemCount: items.length, padding: const EdgeInsets.symmetric(vertical: 16), itemBuilder: (BuildContext context, int index) { return Dismissible( background: Container( color: Colors.green, ), key: ValueKey<int>(items[index]), onDismissed: (DismissDirection direction) { setState(() { items.removeAt(index); }); }, child: ListTile( title: Text( 'Item ${items[index]}', ), ), ); }, ); }
Checkpoint: Complete this tutorial on how to implement swipe to dismiss using the dismissible widget.
API Docs: Dismissible
Looking for more widgets?
#This page features just a few of the common Material widgets that you can use for handling user input in your Flutter app. Check out the Material Widget library and Material Library API docs for a full list of widgets.
Demo: See Flutter's Material 3 Demo for a curated sample of user input widgets available in the Material library.
If the Material and Cupertino libraries don't have a widget that does what you need, check out pub.dev to find Flutter & Dart community-owned and maintained packages. For example, the flutter_slidable package provides a Slidable widget that is more customizable than the Dismissible widget described in the previous section.
Build interactive widgets with GestureDetector
# Have you scoured the widget libraries, pub.dev, asked your coding friends, and still can't find a widget that fits the user interaction that you're looking for? You can build your own custom widget and make it interactive using GestureDetector.
Checkpoint: Use this recipe as a starting point to create your own custom button widget that can handle taps.
Reference: Check out Taps, drags, and other gestures which explains how to listen for, and respond to, gestures in Flutter.
Bonus Video: Curious how Flutter's
GestureArenaturns raw user interaction data into human recognizable concepts like taps, drags, and pinches? Check out this video: GestureArena (Decoding Flutter)
Don't forget about accessibility!
# If you're building a custom widget, annotate its meaning with the Semantics widget. It provides descriptions and metadata to screen readers and other semantic analysis-based tools.
API Docs: GestureDetector • Semantics
Testing
#Once you have finished building user interactions into your app, don't forget to write tests to ensure that everything works as expected!
These tutorials walk you through writing tests that simulate user interactions in your app:
Checkpoint: Follow this tap, drag, and enter text cookbook article and learn how to use
WidgetTesterto simulate and test user interactions in your app.
Bonus Tutorial: The handle scrolling cookbook recipe shows you how to verify that lists of widgets contain the expected content by scrolling through the lists using widget tests.
Next: Networking
#This page was an introduction to handling user input. Now that you know how to handle input from app users, you can make your app even more interesting by adding external data. In the next section, you'll learn how to fetch data for your app over a network, how to convert data to and from JSON, authentication, and other networking features.
Feedback
#As this section of the website is evolving, we welcome your feedback!
Unless stated otherwise, the documentation on this site reflects Flutter 3.35.5. Page last updated on 2025-10-28. View source or report an issue.