Skip to content

Commit 5a229e2

Browse files
authored
Add LookupBoundary to Overlay (#116741)
* Add LookupBoundary to Overlay * fix analysis
1 parent a8c9f9c commit 5a229e2

File tree

5 files changed

+221
-8
lines changed

5 files changed

+221
-8
lines changed

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

+9-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart';
1010
import 'basic.dart';
1111
import 'framework.dart';
1212
import 'localizations.dart';
13+
import 'lookup_boundary.dart';
1314
import 'media_query.dart';
1415
import 'overlay.dart';
1516
import 'table.dart';
@@ -468,12 +469,17 @@ bool debugCheckHasWidgetsLocalizations(BuildContext context) {
468469
/// Does nothing if asserts are disabled. Always returns true.
469470
bool debugCheckHasOverlay(BuildContext context) {
470471
assert(() {
471-
if (context.widget is! Overlay && context.findAncestorWidgetOfExactType<Overlay>() == null) {
472+
if (LookupBoundary.findAncestorWidgetOfExactType<Overlay>(context) == null) {
473+
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Overlay>(context);
472474
throw FlutterError.fromParts(<DiagnosticsNode>[
473-
ErrorSummary('No Overlay widget found.'),
475+
ErrorSummary('No Overlay widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
476+
if (hiddenByBoundary)
477+
ErrorDescription(
478+
'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.'
479+
),
474480
ErrorDescription(
475481
'${context.widget.runtimeType} widgets require an Overlay '
476-
'widget ancestor.\n'
482+
'widget ancestor within the closest LookupBoundary.\n'
477483
'An overlay lets widgets float on top of other widget children.',
478484
),
479485
ErrorHint(

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

+23
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,29 @@ class LookupBoundary extends InheritedWidget {
273273
return result!;
274274
}
275275

276+
/// Returns true if a [LookupBoundary] is hiding the nearest [StatefulWidget]
277+
/// with a [State] of the specified type `T` from the provided [BuildContext].
278+
///
279+
/// This method throws when asserts are disabled.
280+
static bool debugIsHidingAncestorStateOfType<T extends State>(BuildContext context) {
281+
bool? result;
282+
assert(() {
283+
bool hiddenByBoundary = false;
284+
bool ancestorFound = false;
285+
context.visitAncestorElements((Element ancestor) {
286+
if (ancestor is StatefulElement && ancestor.state is T) {
287+
ancestorFound = true;
288+
return false;
289+
}
290+
hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
291+
return true;
292+
});
293+
result = ancestorFound & hiddenByBoundary;
294+
return true;
295+
} ());
296+
return result!;
297+
}
298+
276299
/// Returns true if a [LookupBoundary] is hiding the nearest
277300
/// [RenderObjectWidget] with a [RenderObject] of the specified type `T`
278301
/// from the provided [BuildContext].

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

+12-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:flutter/scheduler.dart';
1111

1212
import 'basic.dart';
1313
import 'framework.dart';
14+
import 'lookup_boundary.dart';
1415
import 'ticker_provider.dart';
1516

1617
// Examples can assume:
@@ -338,7 +339,8 @@ class Overlay extends StatefulWidget {
338339
final Clip clipBehavior;
339340

340341
/// The [OverlayState] from the closest instance of [Overlay] that encloses
341-
/// the given context, and, in debug mode, will throw if one is not found.
342+
/// the given context within the closest [LookupBoundary], and, in debug mode,
343+
/// will throw if one is not found.
342344
///
343345
/// In debug mode, if the `debugRequiredFor` argument is provided and an
344346
/// overlay isn't found, then this function will throw an exception containing
@@ -372,8 +374,13 @@ class Overlay extends StatefulWidget {
372374
final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay);
373375
assert(() {
374376
if (result == null) {
377+
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorStateOfType<OverlayState>(context);
375378
final List<DiagnosticsNode> information = <DiagnosticsNode>[
376-
ErrorSummary('No Overlay widget found.'),
379+
ErrorSummary('No Overlay widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
380+
if (hiddenByBoundary)
381+
ErrorDescription(
382+
'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.'
383+
),
377384
ErrorDescription('${debugRequiredFor?.runtimeType ?? 'Some'} widgets require an Overlay widget ancestor for correct operation.'),
378385
ErrorHint('The most common way to add an Overlay to an application is to include a MaterialApp, CupertinoApp or Navigator widget in the runApp() call.'),
379386
if (debugRequiredFor != null) DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty),
@@ -389,7 +396,7 @@ class Overlay extends StatefulWidget {
389396
}
390397

391398
/// The [OverlayState] from the closest instance of [Overlay] that encloses
392-
/// the given context, if any.
399+
/// the given context within the closest [LookupBoundary], if any.
393400
///
394401
/// Typical usage is as follows:
395402
///
@@ -413,8 +420,8 @@ class Overlay extends StatefulWidget {
413420
bool rootOverlay = false,
414421
}) {
415422
return rootOverlay
416-
? context.findRootAncestorStateOfType<OverlayState>()
417-
: context.findAncestorStateOfType<OverlayState>();
423+
? LookupBoundary.findRootAncestorStateOfType<OverlayState>(context)
424+
: LookupBoundary.findAncestorStateOfType<OverlayState>(context);
418425
}
419426

420427
@override

packages/flutter/test/widgets/lookup_boundary_test.dart

+58
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,64 @@ void main() {
10211021
});
10221022
});
10231023

1024+
group('LookupBoundary.debugIsHidingAncestorStateOfType', () {
1025+
testWidgets('is hiding', (WidgetTester tester) async {
1026+
bool? isHidden;
1027+
await tester.pumpWidget(MyStatefulContainer(
1028+
child: LookupBoundary(
1029+
child: Builder(
1030+
builder: (BuildContext context) {
1031+
isHidden = LookupBoundary.debugIsHidingAncestorStateOfType<MyStatefulContainerState>(context);
1032+
return Container();
1033+
},
1034+
),
1035+
),
1036+
));
1037+
expect(isHidden, isTrue);
1038+
});
1039+
1040+
testWidgets('is not hiding entity within boundary', (WidgetTester tester) async {
1041+
bool? isHidden;
1042+
await tester.pumpWidget(MyStatefulContainer(
1043+
child: LookupBoundary(
1044+
child: MyStatefulContainer(
1045+
child: Builder(
1046+
builder: (BuildContext context) {
1047+
isHidden = LookupBoundary.debugIsHidingAncestorStateOfType<MyStatefulContainerState>(context);
1048+
return Container();
1049+
},
1050+
),
1051+
),
1052+
),
1053+
));
1054+
expect(isHidden, isFalse);
1055+
});
1056+
1057+
testWidgets('is not hiding if no boundary exists', (WidgetTester tester) async {
1058+
bool? isHidden;
1059+
await tester.pumpWidget(MyStatefulContainer(
1060+
child: Builder(
1061+
builder: (BuildContext context) {
1062+
isHidden = LookupBoundary.debugIsHidingAncestorStateOfType<MyStatefulContainerState>(context);
1063+
return Container();
1064+
},
1065+
),
1066+
));
1067+
expect(isHidden, isFalse);
1068+
});
1069+
1070+
testWidgets('is not hiding if no boundary and no entity exists', (WidgetTester tester) async {
1071+
bool? isHidden;
1072+
await tester.pumpWidget(Builder(
1073+
builder: (BuildContext context) {
1074+
isHidden = LookupBoundary.debugIsHidingAncestorStateOfType<MyStatefulContainerState>(context);
1075+
return Container();
1076+
},
1077+
));
1078+
expect(isHidden, isFalse);
1079+
});
1080+
});
1081+
10241082
group('LookupBoundary.debugIsHidingAncestorRenderObjectOfType', () {
10251083
testWidgets('is hiding', (WidgetTester tester) async {
10261084
bool? isHidden;

packages/flutter/test/widgets/overlay_test.dart

+119
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,125 @@ void main() {
12271227
expect(error, isAssertionError);
12281228
});
12291229
});
1230+
1231+
group('LookupBoundary', () {
1232+
testWidgets('hides Overlay from Overlay.maybeOf', (WidgetTester tester) async {
1233+
OverlayState? overlay;
1234+
1235+
await tester.pumpWidget(
1236+
Directionality(
1237+
textDirection: TextDirection.ltr,
1238+
child: Overlay(
1239+
initialEntries: <OverlayEntry>[
1240+
OverlayEntry(
1241+
builder: (BuildContext context) {
1242+
return LookupBoundary(
1243+
child: Builder(
1244+
builder: (BuildContext context) {
1245+
overlay = Overlay.maybeOf(context);
1246+
return Container();
1247+
},
1248+
),
1249+
);
1250+
},
1251+
),
1252+
],
1253+
),
1254+
),
1255+
);
1256+
1257+
expect(overlay, isNull);
1258+
});
1259+
1260+
testWidgets('hides Overlay from Overlay.of', (WidgetTester tester) async {
1261+
await tester.pumpWidget(
1262+
Directionality(
1263+
textDirection: TextDirection.ltr,
1264+
child: Overlay(
1265+
initialEntries: <OverlayEntry>[
1266+
OverlayEntry(
1267+
builder: (BuildContext context) {
1268+
return LookupBoundary(
1269+
child: Builder(
1270+
builder: (BuildContext context) {
1271+
Overlay.of(context);
1272+
return Container();
1273+
},
1274+
),
1275+
);
1276+
},
1277+
),
1278+
],
1279+
),
1280+
),
1281+
);
1282+
final Object? exception = tester.takeException();
1283+
expect(exception, isFlutterError);
1284+
final FlutterError error = exception! as FlutterError;
1285+
1286+
expect(
1287+
error.toStringDeep(),
1288+
'FlutterError\n'
1289+
' No Overlay widget found within the closest LookupBoundary.\n'
1290+
' There is an ancestor Overlay widget, but it is hidden by a\n'
1291+
' LookupBoundary.\n'
1292+
' Some widgets require an Overlay widget ancestor for correct\n'
1293+
' operation.\n'
1294+
' The most common way to add an Overlay to an application is to\n'
1295+
' include a MaterialApp, CupertinoApp or Navigator widget in the\n'
1296+
' runApp() call.\n'
1297+
' The context from which that widget was searching for an overlay\n'
1298+
' was:\n'
1299+
' Builder\n'
1300+
);
1301+
});
1302+
1303+
testWidgets('hides Overlay from debugCheckHasOverlay', (WidgetTester tester) async {
1304+
await tester.pumpWidget(
1305+
Directionality(
1306+
textDirection: TextDirection.ltr,
1307+
child: Overlay(
1308+
initialEntries: <OverlayEntry>[
1309+
OverlayEntry(
1310+
builder: (BuildContext context) {
1311+
return LookupBoundary(
1312+
child: Builder(
1313+
builder: (BuildContext context) {
1314+
debugCheckHasOverlay(context);
1315+
return Container();
1316+
},
1317+
),
1318+
);
1319+
},
1320+
),
1321+
],
1322+
),
1323+
),
1324+
);
1325+
final Object? exception = tester.takeException();
1326+
expect(exception, isFlutterError);
1327+
final FlutterError error = exception! as FlutterError;
1328+
1329+
expect(
1330+
error.toStringDeep(), startsWith(
1331+
'FlutterError\n'
1332+
' No Overlay widget found within the closest LookupBoundary.\n'
1333+
' There is an ancestor Overlay widget, but it is hidden by a\n'
1334+
' LookupBoundary.\n'
1335+
' Builder widgets require an Overlay widget ancestor within the\n'
1336+
' closest LookupBoundary.\n'
1337+
' An overlay lets widgets float on top of other widget children.\n'
1338+
' To introduce an Overlay widget, you can either directly include\n'
1339+
' one, or use a widget that contains an Overlay itself, such as a\n'
1340+
' Navigator, WidgetApp, MaterialApp, or CupertinoApp.\n'
1341+
' The specific widget that could not find a Overlay ancestor was:\n'
1342+
' Builder\n'
1343+
' The ancestors of this widget were:\n'
1344+
' LookupBoundary\n'
1345+
),
1346+
);
1347+
});
1348+
});
12301349
}
12311350

12321351
class StatefulTestWidget extends StatefulWidget {

0 commit comments

Comments
 (0)