Flutter Dropdown Button: 3 Ways to Build a Dropdown (2026)
Last Updated: | Author: Deep Singh
Dropdowns are one of those widgets you'll use in almost every form. Choosing a country, a category, a language, a payment method — a dropdown keeps the UI clean while giving users a fixed set of options to pick from.
Flutter offers three main ways to build a dropdown: the plain DropdownButton, the form-friendly DropdownButtonFormField, and the under-line-free DropdownButtonHideUnderline. In this tutorial, we'll build all three side by side so you can see the differences and pick the right one for your project.
The Three Dropdown Widgets
Before writing any code, let's understand what each widget is for:
- DropdownButton — the base widget. You give it a list of items, a currently selected value, and an
onChangedcallback. It renders a text button that opens a menu when tapped. - DropdownButtonFormField — a
DropdownButtonwrapped in aFormField, so it participates in form validation. It also accepts anInputDecoration, giving you the same look as aTextField. - DropdownButtonHideUnderline — a wrapper that removes the default underline decoration from a
DropdownButton. Useful when you want the dropdown to blend into a custom layout.
The Full Code
Here's the complete, runnable example. It builds all three dropdown widgets sharing a single selection state so you can see them stay in sync.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Body(),
);
}
}
class Body extends StatefulWidget {
const Body({super.key});
@override
State<Body> createState() => _BodyState();
}
class _BodyState extends State<Body> {
String? _selectedCountry;
static const List<String> _countries = [
'India',
'America',
'Nepal',
'Russia',
'Germany',
];
List<DropdownMenuItem<String>> _buildItems() {
return _countries.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList();
}
void _onChanged(String? value) {
setState(() {
_selectedCountry = value;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Drop Down Tutorial'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_defaultDropdownButton(),
const SizedBox(height: 20),
_formFieldDropdownButton(),
const SizedBox(height: 20),
_underlineHiddenDropdownButton(),
],
),
),
);
}
Widget _defaultDropdownButton() {
return DropdownButton<String>(
value: _selectedCountry,
isExpanded: true,
style: const TextStyle(color: Colors.blue),
hint: const Text(
'Please select country',
style: TextStyle(color: Colors.blue, fontSize: 15),
),
items: _buildItems(),
onChanged: _onChanged,
);
}
Widget _formFieldDropdownButton() {
return DropdownButtonFormField<String>(
value: _selectedCountry,
hint: const Text('Select Country'),
items: _buildItems(),
onChanged: _onChanged,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
),
),
);
}
Widget _underlineHiddenDropdownButton() {
return DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedCountry,
isExpanded: true,
style: const TextStyle(color: Colors.blue),
hint: const Text(
'Please select country',
style: TextStyle(color: Colors.blue, fontSize: 15),
),
items: _buildItems(),
onChanged: _onChanged,
),
);
}
}
How the Code Works
Let's walk through the key parts.
1. Shared Selection State
String? _selectedCountry;
All three dropdowns read from and write to the same _selectedCountry field. That's why selecting an option in one dropdown updates the others — they're all bound to the same state.
2. Reusable Item List
List<DropdownMenuItem<String>> _buildItems() { ... }
Instead of copying the same .map().toList() chain three times, we extract it into a helper method. If you ever need to change the option list or the item layout, you only change it in one place.
3. The Shared _onChanged Handler
void _onChanged(String? value) {
setState(() {
_selectedCountry = value;
});
}
All three dropdowns use the same callback. The parameter is nullable because onChanged can be called with null when the dropdown is disabled or cleared. We accept it and store it as-is.
4. DropdownButton
The plain DropdownButton is the base. It has no built-in decoration — no border, no validation. It's ideal for toolbars, filters, or any inline selector.
The isExpanded: true property makes the dropdown take the full available width instead of shrinking to fit the selected text. If you don't set it and the items have short labels, the button will be quite narrow.
5. DropdownButtonFormField
This version wraps the dropdown in a FormField, which means it supports validator, gets automatically reset by Form.reset(), and integrates with an outer Form widget. It also accepts an InputDecoration, so you can add borders, labels, and icons just like a TextField.
The example uses OutlineInputBorder to give it a rounded rectangle outline — the same look you'd get from a Material text field.
6. DropdownButtonHideUnderline
By default, DropdownButton draws a thin underline beneath it, which is not always what you want. Wrapping it in DropdownButtonHideUnderline removes that line. This is common when you're placing the dropdown inside a card, a custom container, or a bottom sheet where the underline would look out of place.
Which One Should You Use?
Here's a quick decision guide:
- Use
DropdownButtonFormFieldwhen the dropdown is part of a form that needs validation. It's the closest thing to a Material text field and works with theFormwidget out of the box. - Use
DropdownButtonfor a standalone dropdown that doesn't need validation — a filter, a language switcher, a sort selector. - Use
DropdownButtonHideUnderlinewhen you want the dropdown to blend into a custom design. Wrap a regularDropdownButtonand the underline disappears. - Consider a
SegmentedButton(Flutter 3.7+) if you have only 2–3 options and want them all visible at once. It's often better UX than a dropdown for short lists. - Consider
PopupMenuButtonfor action menus — like a "⋮" button that opens a list of actions. It behaves similarly to a dropdown but is meant for actions, not selection.
Common Pitfalls
- Duplicate values crash the dropdown. If two
DropdownMenuItems share the samevalue, Flutter will throw an assertion. This often happens with dynamic data — make sure your keys are unique. - The selected value must exist in
items. Ifvalueis set to something that isn't in the list, Flutter throws. Usenullor check for the value before passing it. isExpanded: trueprevents overflow. Without it, long labels push against the arrow icon and can cause a "RenderFlex overflowed" error.- Call
setStateinonChanged. If you forget, the selected value won't update visually and the dropdown will appear stuck. - Don't use
DropdownButtoninside aRowwithout constraints. A Row gives its children unbounded width, which breaks the dropdown's layout. Wrap it in anExpandedor a sized container. - Remember to dispose of any associated controllers. Dropdowns themselves don't need disposal, but if you're storing state in a controller, don't leak it.
- Use
String?notString. The value can be null before selection. If you declareString _selectedCountry;without a default, null safety will complain.
Wrapping Up
You now know three ways to build dropdowns in Flutter, and when to reach for each one. The DropdownButtonFormField is the best default for forms, DropdownButton handles the simple cases, and DropdownButtonHideUnderline gives you full control over the visual style.
From here, you can extend these examples by:
- Adding a
validatortoDropdownButtonFormFieldto require a selection. - Building the item list dynamically from an API instead of a hard-coded constant.
- Grouping items with
DropdownMenuItem's siblings or by using a custom item builder. - Styling the dropdown menu itself using the
dropdownColorandmenuMaxHeightproperties.
No comments:
Post a Comment