iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
💧

DropdownButton Errors and Solutions

に公開

Many of you who use DropdownButton likely encounter the following errors frequently.

Error 1:

'package:flutter/src/material/dropdown.dart':
Failed assertion: line 1258 pos 12:
'widget.items!.where((DropdownMenuItem<T> item) => item.value == widget.value).length == 1': is not true.

Error 2:

There should be exactly one item with [DropdownButton]'s value: unknown.
Either zero or 2 or more [DropdownMenuItem]s were detected with the same value
'package:flutter/src/material/dropdown.dart':
Failed assertion: line 890 pos 15: 'items == null || items.isEmpty || value == null ||
              items.where((DropdownMenuItem<T> item) {
                return item.value == value;
              }).length == 1'

Since these errors are critical to rendering, the screen will not be displayed when they occur. While many might have a vague understanding of what the error messages mean, they often don't know how to fix them.

These errors are caused by an issue during the comparison between DropdownButton.value and each DropdownMenuItem.value. Why are they compared? This is because DropdownButton needs to have one of the DropdownMenuItems (selection items) in a selected state during rendering. A dropdown button is pointless if the result of a user's selection isn't reflected in the UI. Once you understand that, the rest is simple.

'widget.items!.where((DropdownMenuItem<T> item) => item.value == widget.value).length == 1': is not true.

First, let's look at the first error message. widget refers to the DropdownButton, and items is the list of DropdownMenuItems. An error occurs if the comparison of the value property between the DropdownButton and each DropdownMenuItem does not result in exactly one match.

In other words, it means either multiple items were found that should be in a selected state, or none were found at all. Since DropdownButton displays only one item, this results in an error. Try adding a debug print to check the contents of the value you passed to DropdownMenuItem. You will likely see the same value appearing multiple times.

To resolve this, you must ensure that only one item is set to be selected—meaning DropdownButton.value must match exactly one of the DropdownMenuItem.values.

There should be exactly one item with [DropdownButton]'s value: unknown. Either zero or 2 or more [DropdownMenuItem]s were detected with the same value

The second error message indicates that the value of the DropdownButton is unknown, or 0, 2, or more DropdownMenuItems were detected. The meaning is exactly the same as the first error message, and both the cause and the solution are the same. It's likely just that the error messages have not been unified.

Pitfalls when using custom classes

Even if you understand the details of these errors, you might still run into issues if the DropdownButton and DropdownMenuItem do not share "identical" objects. By "identical," I mean objects where the comparison using == results in true. This is a common pitfall when trying to pass custom classes to the value property.

For example, let's say you pass objects of the following class to the value property. It's a simple class with a single string property.

class Person {
  Person(this.name);
  String name;
}

Below is an example of passing Person objects to a DropdownButton.

class _MyHomePageState extends State<MyHomePage> {

  // Dropdown button selection items
  static const menuItemValues = [
    Person('Alice'),
    Person('Bob'),
    Person('Carol'),
  ];

  // Selected item
  // Assume the first item is set by default
  Person? _selected = menuItemValues[0];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: DropdownButton(
            value: _selected,
            items: menuItemValues.map(
              (value) {
                return DropdownMenuItem(
                  value: value,
                  child: Text('${value.name}'),
                );
              },
            ).toList(),
            onChanged: (value) {
              setState(() {
                _selected = value;
              });
            }),
      ),
    );
  }
}

The code above will render without any issues because both the DropdownButton and DropdownMenuItem refer to the same objects in menuItemValues.

A common mistake is having different objects with the same content. For instance, if you create a new Person object with name == 'Alice' and pass it to the DropdownButton, it will not match any of the objects in menuItemValues, resulting in an error. This is because the implementation of == in Object (the superclass of Person) only returns true for the exact same instance, so the newly created Person won't match any object in menuItemValues.

Example:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: DropdownButton(
            // Even if the content is the same, it doesn't match because a new object is created
            value: Person('Alice'),
            items: menuItemValues.map(
              (value) {
                return DropdownMenuItem(
                  value: value,
                  child: Text('${value.name}'),
                );
              },
            ).toList(),
            onChanged: (value) {
              setState(() {
                _selected = value;
              });
            }),
      ),
    );
  }
}

If you want the comparison to be based solely on the content of the Person class, you should override == and hashCode. hashCode is a property that should always be overridden along with ==.

class Person {
  Person(this.name);
  String name;

  @override
  bool operator ==(Object other) {
    return other is Person && name == other.name;
  }

  @override
  int get hashCode {
    return name.hashCode;
  }
}

However, this approach is not recommended if you plan to use the Person class outside of the UI as well. Implementing == manually in every custom class can often lead to bugs. Instead, it's better to prepare data classes for the UI using libraries like freezed, which automatically generate == implementations, or IDE plugins like the Dart Data Class plugin. It would be ideal if Dart supported data classes natively.

That's all.

Discussion