Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 9dd3087

Browse files
authored
Add LookupBoundary to Material (#116736)
1 parent 332032d commit 9dd3087

File tree

6 files changed

+291
-8
lines changed

6 files changed

+291
-8
lines changed

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger;
1111
// Examples can assume:
1212
// late BuildContext context;
1313

14-
/// Asserts that the given context has a [Material] ancestor.
14+
/// Asserts that the given context has a [Material] ancestor within the closest
15+
/// [LookupBoundary].
1516
///
1617
/// Used by many Material Design widgets to make sure that they are
1718
/// only used in contexts where they can print ink onto some material.
@@ -32,12 +33,17 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger;
3233
/// Does nothing if asserts are disabled. Always returns true.
3334
bool debugCheckHasMaterial(BuildContext context) {
3435
assert(() {
35-
if (context.widget is! Material && context.findAncestorWidgetOfExactType<Material>() == null) {
36+
if (LookupBoundary.findAncestorWidgetOfExactType<Material>(context) == null) {
37+
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Material>(context);
3638
throw FlutterError.fromParts(<DiagnosticsNode>[
37-
ErrorSummary('No Material widget found.'),
39+
ErrorSummary('No Material widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
40+
if (hiddenByBoundary)
41+
ErrorDescription(
42+
'There is an ancestor Material widget, but it is hidden by a LookupBoundary.'
43+
),
3844
ErrorDescription(
3945
'${context.widget.runtimeType} widgets require a Material '
40-
'widget ancestor.\n'
46+
'widget ancestor within the closest LookupBoundary.\n'
4147
'In Material Design, most widgets are conceptually "printed" on '
4248
"a sheet of material. In Flutter's material library, that "
4349
'material is represented by the Material widget. It is the '

packages/flutter/lib/src/material/material.dart

+13-3
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ class Material extends StatefulWidget {
343343
final BorderRadiusGeometry? borderRadius;
344344

345345
/// The ink controller from the closest instance of this class that
346-
/// encloses the given context.
346+
/// encloses the given context within the closest [LookupBoundary].
347347
///
348348
/// Typical usage is as follows:
349349
///
@@ -358,11 +358,11 @@ class Material extends StatefulWidget {
358358
/// * [Material.of], which is similar to this method, but asserts if
359359
/// no [Material] ancestor is found.
360360
static MaterialInkController? maybeOf(BuildContext context) {
361-
return context.findAncestorRenderObjectOfType<_RenderInkFeatures>();
361+
return LookupBoundary.findAncestorRenderObjectOfType<_RenderInkFeatures>(context);
362362
}
363363

364364
/// The ink controller from the closest instance of [Material] that encloses
365-
/// the given context.
365+
/// the given context within the closest [LookupBoundary].
366366
///
367367
/// If no [Material] widget ancestor can be found then this method will assert
368368
/// in debug mode, and throw an exception in release mode.
@@ -383,6 +383,16 @@ class Material extends StatefulWidget {
383383
final MaterialInkController? controller = maybeOf(context);
384384
assert(() {
385385
if (controller == null) {
386+
if (LookupBoundary.debugIsHidingAncestorRenderObjectOfType<_RenderInkFeatures>(context)) {
387+
throw FlutterError(
388+
'Material.of() was called with a context that does not have access to a Material widget.\n'
389+
'The context provided to Material.of() does have a Material widget ancestor, but it is '
390+
'hidden by a LookupBoundary. This can happen because you are using a widget that looks '
391+
'for a Material ancestor, but no such ancestor exists within the closest LookupBoundary.\n'
392+
'The context used was:\n'
393+
' $context',
394+
);
395+
}
386396
throw FlutterError(
387397
'Material.of() was called with a context that does not contain a Material widget.\n'
388398
'No Material widget ancestor could be found starting from the context that was passed to '

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

+47
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,53 @@ class LookupBoundary extends InheritedWidget {
250250
});
251251
}
252252

253+
/// Returns true if a [LookupBoundary] is hiding the nearest
254+
/// [Widget] of the specified type `T` from the provided [BuildContext].
255+
///
256+
/// This method throws when asserts are disabled.
257+
static bool debugIsHidingAncestorWidgetOfExactType<T extends Widget>(BuildContext context) {
258+
bool? result;
259+
assert(() {
260+
bool hiddenByBoundary = false;
261+
bool ancestorFound = false;
262+
context.visitAncestorElements((Element ancestor) {
263+
if (ancestor.widget.runtimeType == T) {
264+
ancestorFound = true;
265+
return false;
266+
}
267+
hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
268+
return true;
269+
});
270+
result = ancestorFound & hiddenByBoundary;
271+
return true;
272+
} ());
273+
return result!;
274+
}
275+
276+
/// Returns true if a [LookupBoundary] is hiding the nearest
277+
/// [RenderObjectWidget] with a [RenderObject] of the specified type `T`
278+
/// from the provided [BuildContext].
279+
///
280+
/// This method throws when asserts are disabled.
281+
static bool debugIsHidingAncestorRenderObjectOfType<T extends RenderObject>(BuildContext context) {
282+
bool? result;
283+
assert(() {
284+
bool hiddenByBoundary = false;
285+
bool ancestorFound = false;
286+
context.visitAncestorElements((Element ancestor) {
287+
if (ancestor is RenderObjectElement && ancestor.renderObject is T) {
288+
ancestorFound = true;
289+
return false;
290+
}
291+
hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
292+
return true;
293+
});
294+
result = ancestorFound & hiddenByBoundary;
295+
return true;
296+
} ());
297+
return result!;
298+
}
299+
253300
@override
254301
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
255302
}

packages/flutter/test/material/debug_test.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ void main() {
2828
error.toStringDeep(),
2929
'FlutterError\n'
3030
' No Material widget found.\n'
31-
' Chip widgets require a Material widget ancestor.\n'
31+
' Chip widgets require a Material widget ancestor within the\n'
32+
' closest LookupBoundary.\n'
3233
' In Material Design, most widgets are conceptually "printed" on a\n'
3334
" sheet of material. In Flutter's material library, that material\n"
3435
' is represented by the Material widget. It is the Material widget\n'

packages/flutter/test/material/material_test.dart

+95
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,101 @@ void main() {
10341034
materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero);
10351035
expect(tracker.paintCount, 2);
10361036
});
1037+
1038+
group('LookupBoundary', () {
1039+
testWidgets('hides Material from Material.maybeOf', (WidgetTester tester) async {
1040+
MaterialInkController? material;
1041+
1042+
await tester.pumpWidget(
1043+
Material(
1044+
child: LookupBoundary(
1045+
child: Builder(
1046+
builder: (BuildContext context) {
1047+
material = Material.maybeOf(context);
1048+
return Container();
1049+
},
1050+
),
1051+
),
1052+
),
1053+
);
1054+
1055+
expect(material, isNull);
1056+
});
1057+
1058+
testWidgets('hides Material from Material.of', (WidgetTester tester) async {
1059+
await tester.pumpWidget(
1060+
Material(
1061+
child: LookupBoundary(
1062+
child: Builder(
1063+
builder: (BuildContext context) {
1064+
Material.of(context);
1065+
return Container();
1066+
},
1067+
),
1068+
),
1069+
),
1070+
);
1071+
final Object? exception = tester.takeException();
1072+
expect(exception, isFlutterError);
1073+
final FlutterError error = exception! as FlutterError;
1074+
1075+
expect(
1076+
error.toStringDeep(),
1077+
'FlutterError\n'
1078+
' Material.of() was called with a context that does not have access\n'
1079+
' to a Material widget.\n'
1080+
' The context provided to Material.of() does have a Material widget\n'
1081+
' ancestor, but it is hidden by a LookupBoundary. This can happen\n'
1082+
' because you are using a widget that looks for a Material\n'
1083+
' ancestor, but no such ancestor exists within the closest\n'
1084+
' LookupBoundary.\n'
1085+
' The context used was:\n'
1086+
' Builder(dirty)\n'
1087+
);
1088+
});
1089+
1090+
testWidgets('hides Material from debugCheckHasMaterial', (WidgetTester tester) async {
1091+
await tester.pumpWidget(
1092+
Material(
1093+
child: LookupBoundary(
1094+
child: Builder(
1095+
builder: (BuildContext context) {
1096+
debugCheckHasMaterial(context);
1097+
return Container();
1098+
},
1099+
),
1100+
),
1101+
),
1102+
);
1103+
final Object? exception = tester.takeException();
1104+
expect(exception, isFlutterError);
1105+
final FlutterError error = exception! as FlutterError;
1106+
1107+
expect(
1108+
error.toStringDeep(), startsWith(
1109+
'FlutterError\n'
1110+
' No Material widget found within the closest LookupBoundary.\n'
1111+
' There is an ancestor Material widget, but it is hidden by a\n'
1112+
' LookupBoundary.\n'
1113+
' Builder widgets require a Material widget ancestor within the\n'
1114+
' closest LookupBoundary.\n'
1115+
' In Material Design, most widgets are conceptually "printed" on a\n'
1116+
" sheet of material. In Flutter's material library, that material\n"
1117+
' is represented by the Material widget. It is the Material widget\n'
1118+
' that renders ink splashes, for instance. Because of this, many\n'
1119+
' material library widgets require that there be a Material widget\n'
1120+
' in the tree above them.\n'
1121+
' To introduce a Material widget, you can either directly include\n'
1122+
' one, or use a widget that contains Material itself, such as a\n'
1123+
' Card, Dialog, Drawer, or Scaffold.\n'
1124+
' The specific widget that could not find a Material ancestor was:\n'
1125+
' Builder\n'
1126+
' The ancestors of this widget were:\n'
1127+
' LookupBoundary\n'
1128+
),
1129+
);
1130+
});
1131+
});
10371132
}
10381133

10391134
class TrackPaintInkFeature extends InkFeature {

packages/flutter/test/widgets/lookup_boundary_test.dart

+124
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,130 @@ void main() {
958958

959959
});
960960
});
961+
962+
group('LookupBoundary.debugIsHidingAncestorWidgetOfExactType', () {
963+
testWidgets('is hiding', (WidgetTester tester) async {
964+
bool? isHidden;
965+
await tester.pumpWidget(Container(
966+
color: Colors.blue,
967+
child: LookupBoundary(
968+
child: Builder(
969+
builder: (BuildContext context) {
970+
isHidden = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Container>(context);
971+
return Container();
972+
},
973+
),
974+
),
975+
));
976+
expect(isHidden, isTrue);
977+
});
978+
979+
testWidgets('is not hiding entity within boundary', (WidgetTester tester) async {
980+
bool? isHidden;
981+
await tester.pumpWidget(Container(
982+
color: Colors.blue,
983+
child: LookupBoundary(
984+
child: Container(
985+
color: Colors.red,
986+
child: Builder(
987+
builder: (BuildContext context) {
988+
isHidden = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Container>(context);
989+
return Container();
990+
},
991+
),
992+
),
993+
),
994+
));
995+
expect(isHidden, isFalse);
996+
});
997+
998+
testWidgets('is not hiding if no boundary exists', (WidgetTester tester) async {
999+
bool? isHidden;
1000+
await tester.pumpWidget(Container(
1001+
color: Colors.blue,
1002+
child: Builder(
1003+
builder: (BuildContext context) {
1004+
isHidden = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Container>(context);
1005+
return Container();
1006+
},
1007+
),
1008+
));
1009+
expect(isHidden, isFalse);
1010+
});
1011+
1012+
testWidgets('is not hiding if no boundary and no entity exists', (WidgetTester tester) async {
1013+
bool? isHidden;
1014+
await tester.pumpWidget(Builder(
1015+
builder: (BuildContext context) {
1016+
isHidden = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Container>(context);
1017+
return Container();
1018+
},
1019+
));
1020+
expect(isHidden, isFalse);
1021+
});
1022+
});
1023+
1024+
group('LookupBoundary.debugIsHidingAncestorRenderObjectOfType', () {
1025+
testWidgets('is hiding', (WidgetTester tester) async {
1026+
bool? isHidden;
1027+
await tester.pumpWidget(Padding(
1028+
padding: EdgeInsets.zero,
1029+
child: LookupBoundary(
1030+
child: Builder(
1031+
builder: (BuildContext context) {
1032+
isHidden = LookupBoundary.debugIsHidingAncestorRenderObjectOfType<RenderPadding>(context);
1033+
return Container();
1034+
},
1035+
),
1036+
),
1037+
));
1038+
expect(isHidden, isTrue);
1039+
});
1040+
1041+
testWidgets('is not hiding entity within boundary', (WidgetTester tester) async {
1042+
bool? isHidden;
1043+
await tester.pumpWidget(Padding(
1044+
padding: EdgeInsets.zero,
1045+
child: LookupBoundary(
1046+
child: Padding(
1047+
padding: EdgeInsets.zero,
1048+
child: Builder(
1049+
builder: (BuildContext context) {
1050+
isHidden = LookupBoundary.debugIsHidingAncestorRenderObjectOfType<RenderPadding>(context);
1051+
return Container();
1052+
},
1053+
),
1054+
),
1055+
),
1056+
));
1057+
expect(isHidden, isFalse);
1058+
});
1059+
1060+
testWidgets('is not hiding if no boundary exists', (WidgetTester tester) async {
1061+
bool? isHidden;
1062+
await tester.pumpWidget(Padding(
1063+
padding: EdgeInsets.zero,
1064+
child: Builder(
1065+
builder: (BuildContext context) {
1066+
isHidden = LookupBoundary.debugIsHidingAncestorRenderObjectOfType<RenderPadding>(context);
1067+
return Container();
1068+
},
1069+
),
1070+
));
1071+
expect(isHidden, isFalse);
1072+
});
1073+
1074+
testWidgets('is not hiding if no boundary and no entity exists', (WidgetTester tester) async {
1075+
bool? isHidden;
1076+
await tester.pumpWidget(Builder(
1077+
builder: (BuildContext context) {
1078+
isHidden = LookupBoundary.debugIsHidingAncestorRenderObjectOfType<RenderPadding>(context);
1079+
return Container();
1080+
},
1081+
));
1082+
expect(isHidden, isFalse);
1083+
});
1084+
});
9611085
}
9621086

9631087
class MyStatefulContainer extends StatefulWidget {

0 commit comments

Comments
 (0)