DropdownButton のエラーとその対策
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