Skip to content

Commit 5de6684

Browse files
Add more info to OverlayState.insert error messages (#129363)
I was debugging an Overlay issue and felt I could have identified the problem faster if the existing assertions provided more information about the current state of the OverlayEntry and Overlay.
1 parent 0bc5a2b commit 5de6684

2 files changed

Lines changed: 119 additions & 12 deletions

File tree

packages/flutter/lib/src/widgets/overlay.dart

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ class OverlayEntry implements Listenable {
222222
}
223223

224224
@override
225-
String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)';
225+
String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)${_disposedByOwner ? "(DISPOSED)" : ""}';
226226
}
227227

228228
class _OverlayEntryWidget extends StatefulWidget {
@@ -296,7 +296,7 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
296296
late final Iterable<RenderBox> _hitTestOrderIterable = _createChildIterable(reversed: true);
297297

298298
// The following uses sync* because hit-testing is lazy, and LinkedList as a
299-
// Iterable doesn't support current modification.
299+
// Iterable doesn't support concurrent modification.
300300
Iterable<RenderBox> _createChildIterable({ required bool reversed }) sync* {
301301
final LinkedList<_OverlayEntryLocation>? children = _sortedTheaterSiblings;
302302
if (children == null || children.isEmpty) {
@@ -543,6 +543,55 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
543543
return _entries.length;
544544
}
545545

546+
bool _debugCanInsertEntry(OverlayEntry entry) {
547+
final List<DiagnosticsNode> operandsInformation = <DiagnosticsNode>[
548+
DiagnosticsProperty<OverlayEntry>('The OverlayEntry was', entry, style: DiagnosticsTreeStyle.errorProperty),
549+
DiagnosticsProperty<OverlayState>(
550+
'The Overlay the OverlayEntry was trying to insert to was', this, style: DiagnosticsTreeStyle.errorProperty,
551+
),
552+
];
553+
554+
if (!mounted) {
555+
throw FlutterError.fromParts(<DiagnosticsNode>[
556+
ErrorSummary('Attempted to insert an OverlayEntry to an already disposed Overlay.'),
557+
...operandsInformation,
558+
]);
559+
}
560+
561+
final OverlayState? currentOverlay = entry._overlay;
562+
final bool alreadyContainsEntry = _entries.contains(entry);
563+
564+
if (alreadyContainsEntry) {
565+
final bool inconsistentOverlayState = !identical(currentOverlay, this);
566+
throw FlutterError.fromParts(<DiagnosticsNode>[
567+
ErrorSummary('The specified entry is already present in the target Overlay.'),
568+
...operandsInformation,
569+
if (inconsistentOverlayState) ErrorHint('This could be an error in the Flutter framework.')
570+
else ErrorHint(
571+
'Consider calling remove on the OverlayEntry before inserting it to a different Overlay, '
572+
'or switching to the OverlayPortal API to avoid manual OverlayEntry management.'
573+
),
574+
if (inconsistentOverlayState) DiagnosticsProperty<OverlayState>(
575+
"The OverlayEntry's current Overlay was", currentOverlay, style: DiagnosticsTreeStyle.errorProperty,
576+
),
577+
]);
578+
}
579+
580+
if (currentOverlay == null) {
581+
return true;
582+
}
583+
584+
throw FlutterError.fromParts(<DiagnosticsNode>[
585+
ErrorSummary('The specified entry is already present in a different Overlay.'),
586+
...operandsInformation,
587+
DiagnosticsProperty<OverlayState>("The OverlayEntry's current Overlay was", currentOverlay, style: DiagnosticsTreeStyle.errorProperty,),
588+
ErrorHint(
589+
'Consider calling remove on the OverlayEntry before inserting it to a different Overlay, '
590+
'or switching to the OverlayPortal API to avoid manual OverlayEntry management.'
591+
)
592+
]);
593+
}
594+
546595
/// Insert the given entry into the overlay.
547596
///
548597
/// If `below` is non-null, the entry is inserted just below `below`.
@@ -552,8 +601,7 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
552601
/// It is an error to specify both `above` and `below`.
553602
void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }) {
554603
assert(_debugVerifyInsertPosition(above, below));
555-
assert(!_entries.contains(entry), 'The specified entry is already present in the Overlay.');
556-
assert(entry._overlay == null, 'The specified entry is already present in another Overlay.');
604+
assert(_debugCanInsertEntry(entry));
557605
entry._overlay = this;
558606
setState(() {
559607
_entries.insert(_insertionIndex(below, above), entry);
@@ -569,14 +617,7 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
569617
/// It is an error to specify both `above` and `below`.
570618
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry? below, OverlayEntry? above }) {
571619
assert(_debugVerifyInsertPosition(above, below));
572-
assert(
573-
entries.every((OverlayEntry entry) => !_entries.contains(entry)),
574-
'One or more of the specified entries are already present in the Overlay.',
575-
);
576-
assert(
577-
entries.every((OverlayEntry entry) => entry._overlay == null),
578-
'One or more of the specified entries are already present in another Overlay.',
579-
);
620+
assert(entries.every(_debugCanInsertEntry));
580621
if (entries.isEmpty) {
581622
return;
582623
}

packages/flutter/test/widgets/overlay_test.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,72 @@ void main() {
11391139
);
11401140
});
11411141

1142+
testWidgets('OverlayEntry throws if inserted to an invalid Overlay', (WidgetTester tester) async {
1143+
await tester.pumpWidget(
1144+
const Directionality(
1145+
textDirection: TextDirection.ltr,
1146+
child: Overlay(),
1147+
),
1148+
);
1149+
final OverlayState overlay = tester.state(find.byType(Overlay));
1150+
final OverlayEntry entry = OverlayEntry(builder: (BuildContext context) => const SizedBox());
1151+
expect(
1152+
() => overlay.insert(entry),
1153+
returnsNormally,
1154+
);
1155+
1156+
// Throws when inserted to the same Overlay.
1157+
expect(
1158+
() => overlay.insert(entry),
1159+
throwsA(isA<FlutterError>().having(
1160+
(FlutterError error) => error.toString(),
1161+
'toString()',
1162+
allOf(
1163+
contains('The specified entry is already present in the target Overlay.'),
1164+
contains('The OverlayEntry was'),
1165+
contains('The Overlay the OverlayEntry was trying to insert to was'),
1166+
),
1167+
)),
1168+
);
1169+
1170+
await tester.pumpWidget(
1171+
const Directionality(
1172+
textDirection: TextDirection.ltr,
1173+
child: SizedBox(child: Overlay()),
1174+
),
1175+
);
1176+
1177+
// Throws if inserted to an already disposed Overlay.
1178+
expect(
1179+
() => overlay.insert(entry),
1180+
throwsA(isA<FlutterError>().having(
1181+
(FlutterError error) => error.toString(),
1182+
'toString()',
1183+
allOf(
1184+
contains('Attempted to insert an OverlayEntry to an already disposed Overlay.'),
1185+
contains('The OverlayEntry was'),
1186+
contains('The Overlay the OverlayEntry was trying to insert to was'),
1187+
),
1188+
)),
1189+
);
1190+
1191+
final OverlayState newOverlay = tester.state(find.byType(Overlay));
1192+
// Throws when inserted to a different Overlay without calling remove.
1193+
expect(
1194+
() => newOverlay.insert(entry),
1195+
throwsA(isA<FlutterError>().having(
1196+
(FlutterError error) => error.toString(),
1197+
'toString()',
1198+
allOf(
1199+
contains('The specified entry is already present in a different Overlay.'),
1200+
contains('The OverlayEntry was'),
1201+
contains('The Overlay the OverlayEntry was trying to insert to was'),
1202+
contains("The OverlayEntry's current Overlay was"),
1203+
),
1204+
)),
1205+
);
1206+
});
1207+
11421208
group('OverlayEntry listenable', () {
11431209
final GlobalKey overlayKey = GlobalKey();
11441210
final Widget emptyOverlay = Directionality(

0 commit comments

Comments
 (0)