💧

DropdownButton のエラーとその対策

2023/02/03に公開

DropdownButton を使っていると、たびたび次のエラーと遭遇する方は多いと思います。

エラーその 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.

エラーその 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'

これらのエラーは描画に関する重要なものなので、発生すると画面が描画されません。エラーメッセージの意味はなんとなくわかるけど、修正の仕方がわからない...という方も多いと思います。

このエラーは、 DropdownButton.value と各 DropdownMenuItem.value の比較の際に問題が発生したのが原因です。なぜ比較するのか?というと、 DropdownButton は描画時に何かしらの DropdownMenuItem (選択項目) を選択した状態にするためです。ユーザーの選択の結果が UI に反映されなければドロップダウンボタンの意味がありませんよね。それがわかれば、あとはシンプルです。

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

まず、 1 つ目のエラーメッセージを見てみましょう。 widget は DropdownButton 、 items は DropdownMenuItem のリストです。 DropdownButton と各 DropdownMenuItem の value プロパティ値を比較し、一致するケースが 1 つではない 場合にエラーとなります。

つまり、選択状態にすべき項目が複数見つかった、もしくは 1 つも見つからなかった、ということです。 DropdownButton が表示する項目は 1 つですから、これはエラーになります。 デバッグプリントでも入れて、 DropdownMenuItem に渡した value の内容を確認してみてください。同じ値が複数表示されてしまっているはずです。

これを解決するには、選択状態にすべき項目を 1 つにする = DropdownButton.value を各 DropdownMenuItem.value のうち 1 つだけに一致する ようにします。

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

2 つ目のエラーメッセージは、 DropdownButton の value が不明のもの。 0 または 2 またはそれ以上の数の DropdownMenuItem が見つかった ことを表します。意味合いとしては 1 つ目のエラーメッセージとまったく同じで、原因も解決策も同じです。エラーメッセージが統一されていないだけでしょうね。

カスタムクラスを使う際のはまりどころ

以上のエラーの詳細がわかっていても解決しない場合は、 DropdownButton と DropdownMenuItem に「同じ」オブジェクトが存在しない可能性があります。「同じ」オブジェクトとは、 == で比較した結果が true になるオブジェクト です。カスタムクラスを value に渡そうとするとはまりやすいポイントです。

たとえば、次のクラスのオブジェクトを value に渡すとします。文字列のプロパティを 1 つ持つだけの単純なクラスです。

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

以下は Person オブジェクトを DropdownButton に渡す例です。

class _MyHomePageState extends State<MyHomePage> {

  // ドロップダウンボタンの選択項目
  static const menuItemValues = [
    Person('Alice'),
    Person('Bob'),
    Person('Carol'),
  ];

  // 選択された項目
  // デフォルトでは最初の項目をセットするとする
  Person? _selected = menuItemValues[0];

  
  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;
              });
            }),
      ),
    );
  }
}

上記のコードは問題なく描画されます。 DropdownButton と DropdownMenuItem で同じ menuItemValues のオブジェクトを参照するからです。

ミスをしやすいのは、 同じ内容で異なるオブジェクト の場合です。たとえば、 name == 'Alice' の Person オブジェクトを新しく生成して DropdownButton に渡すと、いずれの menuItemValues とも一致せずエラーになります。 Person のスーパークラス Object の == の実装は同じオブジェクトでのみ true を返すため、新しく生成した Person はいずれの menuItemValues のオブジェクトとも一致しないわけです。

例:

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: DropdownButton(
            // 内容は同じでも新しいオブジェクトを生成しているので一致しない
            value: Person('Alice'),
            items: menuItemValues.map(
              (value) {
                return DropdownMenuItem(
                  value: value,
                  child: Text('${value.name}'),
                );
              },
            ).toList(),
            onChanged: (value) {
              setState(() {
                _selected = value;
              });
            }),
      ),
    );
  }
}

Person の内容だけで比較して欲しい場合は、 ==hashCode をオーバーライドして内容のみを比較するようにします。 hashCode== と一緒にオーバーライドすべきプロパティです。

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

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

  
  int get hashCode {
    return name.hashCode;
  }
}

ただし、 Person を UI 以外でも使うならこの方法は推奨できません。すべてのカスタムクラスで同様の == を実装するとバグの原因になる場合も多いので、自動的に == の実装を生成してくれる freezed などのライブラリや IDE の Dart Data Class プラグインを使うなりして UI 向けのデータクラスを用意するといいでしょう。 Dart がデータクラスをサポートしてくれればベストなのですが。

以上です。

Discussion