Flutter DropDown Widget Tutorial

Flutter Dropdown Button: 3 Ways to Build a Dropdown (2026) | D.Tech Academic

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 onChanged callback. It renders a text button that opens a menu when tapped.
  • DropdownButtonFormField — a DropdownButton wrapped in a FormField, so it participates in form validation. It also accepts an InputDecoration, giving you the same look as a TextField.
  • DropdownButtonHideUnderline — a wrapper that removes the default underline decoration from a DropdownButton. Useful when you want the dropdown to blend into a custom layout.
Flutter dropdown button examples with default, form field, and underline-free styles
All three dropdown styles rendered side by side

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 DropdownButtonFormField when the dropdown is part of a form that needs validation. It's the closest thing to a Material text field and works with the Form widget out of the box.
  • Use DropdownButton for a standalone dropdown that doesn't need validation — a filter, a language switcher, a sort selector.
  • Use DropdownButtonHideUnderline when you want the dropdown to blend into a custom design. Wrap a regular DropdownButton and 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 PopupMenuButton for 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 same value, Flutter will throw an assertion. This often happens with dynamic data — make sure your keys are unique.
  • The selected value must exist in items. If value is set to something that isn't in the list, Flutter throws. Use null or check for the value before passing it.
  • isExpanded: true prevents overflow. Without it, long labels push against the arrow icon and can cause a "RenderFlex overflowed" error.
  • Call setState in onChanged. If you forget, the selected value won't update visually and the dropdown will appear stuck.
  • Don't use DropdownButton inside a Row without constraints. A Row gives its children unbounded width, which breaks the dropdown's layout. Wrap it in an Expanded or 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? not String. The value can be null before selection. If you declare String _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 validator to DropdownButtonFormField to 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 dropdownColor and menuMaxHeight properties.

No comments: