
ExpansionTile を使用時に _TypeError 例外が発生するときの対処法
2025年4月15日
現象
ExpansionTile
を ListView
の子として配置したとき、
_TypeError (type 'double' is not a subtype of type 'bool?' in type cast)
という例外が発生する。
対処法
ExpansionTileのドキュメントの先頭にも書かれているように
ExpansionTile
を ListView
の子として配置するときは、ExpansionTile
のkey に PageStorageKey
を指定する必要があります。
ListView(
children: [
ExpansionTile(
key: PageStorageKey(someValue),
...
),
],
)
なぜこうする必要があるのか ?
ListView
はスクロール位置をPageStorage
にdouble
値で保存します。
ExpansionTile
は展開されているかどうかを PageStorage
に bool
値で保存します。
ExpansionTile
に PageStorageKey
を指定しない場合、
ListView
と同じkeyに対して状態を保存・読み取ろうとしてしまいます。
PageStorage
(実際はそのデータ保管庫であるPageStorageBucket)に保存するデータの型はdynamic
であり、
状態を保存するときにどの型だったのかわからなくなってしまいます。
したがって、状態を取り出すときに実際の型にキャストする必要があります。
ListView
がdouble
で保存した状態を ExpansionTile
がbool
として読み取ろうとするため、
データのキャスト時に型が合わなくてこのエラーが発生するのです。
_isExpanded =
PageStorage.maybeOf(context)?.readState(context) as bool? ?? widget.initiallyExpanded;
ExpansionTile
のkeyにPageStorageKey
を指定することによって、
ListView
が状態を保存するときに使うkeyとは別のkeyを使ってExpansionTile
が状態を保存・読み取りするようになるので、
型の不一致を防ぐことができるのです。
そもそもPageStorage, PageStorageKeyとは ?
PageStorage
とは、
Widgetの状態を保存する場所です。Widgetは破棄されると状態も失われてしまうため、
PageStorage
に子孫Widgetの状態を保存しておくことで、子孫Widgetが再作成されたときに保存した状態を復元することができます。
PageStorageKey
は何かというと、PageStorage
に状態を保存するときにkeyとして使われるものです。
実際は対象のWidgetに指定したPageStorageKey
だけが使われるのではなく、
Widget位置から親Widgetへとツリーをさかのぼっていき、
PageStorage
にたどり着くまでの間にある祖先のWidgetすべてのkeyに指定されたPageStorageKey
のリストが実際のkeyとなります。
一連の祖先のWidgetも含めたPageStorageKey
のリストによって、ツリー構造が異なればたとえ同じPageStorageKeyが指定されていても異なるkeyということになり、
保存された状態を区別することができるようになるのです。
// 以下の二つのChildWidgetのkeyは別のkeyとして状態が保存される
Column(
children: [
ParentWidget(
key: const PageStorageKey("parent"),
child: ChildWidget(
key: const PageStorageKey("child"),
...
),
),
ChildWidget(
key: const PageStorageKey("child"),
...
),
],
)
ソースを読んでみよう
状態の読み取り:
dynamic readState(BuildContext context, {Object? identifier}) {
if (_storage == null) {
return null;
}
if (identifier != null) {
return _storage![identifier];
}
final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context);
return contextIdentifier.isNotEmpty ? _storage![contextIdentifier] : null;
}
_StorageEntryIdentifier _computeIdentifier(BuildContext context) {
return _StorageEntryIdentifier(_allKeys(context));
}
_StorageEntryIdentifier
というのは、keyのリストを保持して比較できるようにするだけのデータクラスです。
https://github.com/flutter/flutter/blob/99bf419997accdfe6013c1732ce6bc873b01d45f/packages/flutter/lib/src/widgets/page_storage.dart#L41C1-L64C2
祖先のWidgetにさかのぼっていき、PageStorageKeyのリストを作成する
List<PageStorageKey<dynamic>> _allKeys(BuildContext context) {
final List<PageStorageKey<dynamic>> keys = <PageStorageKey<dynamic>>[];
if (_maybeAddKey(context, keys)) {
context.visitAncestorElements((Element element) {
return _maybeAddKey(element, keys);
});
}
return keys;
}
PageStorageにたどり着くまで親Widgetに指定されたPageStorageKeyを集めていく
static bool _maybeAddKey(BuildContext context, List<PageStorageKey<dynamic>> keys) {
final Widget widget = context.widget;
final Key? key = widget.key;
if (key is PageStorageKey) {
keys.add(key);
}
return widget is! PageStorage;
}
