Unverified Commit 9dd30878 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Add LookupBoundary to Material (#116736)

parent 332032dd
...@@ -11,7 +11,8 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger; ...@@ -11,7 +11,8 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger;
// Examples can assume: // Examples can assume:
// late BuildContext context; // late BuildContext context;
/// Asserts that the given context has a [Material] ancestor. /// Asserts that the given context has a [Material] ancestor within the closest
/// [LookupBoundary].
/// ///
/// Used by many Material Design widgets to make sure that they are /// Used by many Material Design widgets to make sure that they are
/// only used in contexts where they can print ink onto some material. /// only used in contexts where they can print ink onto some material.
...@@ -32,12 +33,17 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger; ...@@ -32,12 +33,17 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger;
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasMaterial(BuildContext context) { bool debugCheckHasMaterial(BuildContext context) {
assert(() { assert(() {
if (context.widget is! Material && context.findAncestorWidgetOfExactType<Material>() == null) { if (LookupBoundary.findAncestorWidgetOfExactType<Material>(context) == null) {
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Material>(context);
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('No Material widget found.'), ErrorSummary('No Material widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
if (hiddenByBoundary)
ErrorDescription(
'There is an ancestor Material widget, but it is hidden by a LookupBoundary.'
),
ErrorDescription( ErrorDescription(
'${context.widget.runtimeType} widgets require a Material ' '${context.widget.runtimeType} widgets require a Material '
'widget ancestor.\n' 'widget ancestor within the closest LookupBoundary.\n'
'In Material Design, most widgets are conceptually "printed" on ' 'In Material Design, most widgets are conceptually "printed" on '
"a sheet of material. In Flutter's material library, that " "a sheet of material. In Flutter's material library, that "
'material is represented by the Material widget. It is the ' 'material is represented by the Material widget. It is the '
......
...@@ -343,7 +343,7 @@ class Material extends StatefulWidget { ...@@ -343,7 +343,7 @@ class Material extends StatefulWidget {
final BorderRadiusGeometry? borderRadius; final BorderRadiusGeometry? borderRadius;
/// The ink controller from the closest instance of this class that /// The ink controller from the closest instance of this class that
/// encloses the given context. /// encloses the given context within the closest [LookupBoundary].
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
/// ///
...@@ -358,11 +358,11 @@ class Material extends StatefulWidget { ...@@ -358,11 +358,11 @@ class Material extends StatefulWidget {
/// * [Material.of], which is similar to this method, but asserts if /// * [Material.of], which is similar to this method, but asserts if
/// no [Material] ancestor is found. /// no [Material] ancestor is found.
static MaterialInkController? maybeOf(BuildContext context) { static MaterialInkController? maybeOf(BuildContext context) {
return context.findAncestorRenderObjectOfType<_RenderInkFeatures>(); return LookupBoundary.findAncestorRenderObjectOfType<_RenderInkFeatures>(context);
} }
/// The ink controller from the closest instance of [Material] that encloses /// The ink controller from the closest instance of [Material] that encloses
/// the given context. /// the given context within the closest [LookupBoundary].
/// ///
/// If no [Material] widget ancestor can be found then this method will assert /// If no [Material] widget ancestor can be found then this method will assert
/// in debug mode, and throw an exception in release mode. /// in debug mode, and throw an exception in release mode.
...@@ -383,6 +383,16 @@ class Material extends StatefulWidget { ...@@ -383,6 +383,16 @@ class Material extends StatefulWidget {
final MaterialInkController? controller = maybeOf(context); final MaterialInkController? controller = maybeOf(context);
assert(() { assert(() {
if (controller == null) { if (controller == null) {
if (LookupBoundary.debugIsHidingAncestorRenderObjectOfType<_RenderInkFeatures>(context)) {
throw FlutterError(
'Material.of() was called with a context that does not have access to a Material widget.\n'
'The context provided to Material.of() does have a Material widget ancestor, but it is '
'hidden by a LookupBoundary. This can happen because you are using a widget that looks '
'for a Material ancestor, but no such ancestor exists within the closest LookupBoundary.\n'
'The context used was:\n'
' $context',
);
}
throw FlutterError( throw FlutterError(
'Material.of() was called with a context that does not contain a Material widget.\n' 'Material.of() was called with a context that does not contain a Material widget.\n'
'No Material widget ancestor could be found starting from the context that was passed to ' 'No Material widget ancestor could be found starting from the context that was passed to '
......
...@@ -250,6 +250,53 @@ class LookupBoundary extends InheritedWidget { ...@@ -250,6 +250,53 @@ class LookupBoundary extends InheritedWidget {
}); });
} }
/// Returns true if a [LookupBoundary] is hiding the nearest
/// [Widget] of the specified type `T` from the provided [BuildContext].
///
/// This method throws when asserts are disabled.
static bool debugIsHidingAncestorWidgetOfExactType<T extends Widget>(BuildContext context) {
bool? result;
assert(() {
bool hiddenByBoundary = false;
bool ancestorFound = false;
context.visitAncestorElements((Element ancestor) {
if (ancestor.widget.runtimeType == T) {
ancestorFound = true;
return false;
}
hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
return true;
});
result = ancestorFound & hiddenByBoundary;
return true;
} ());
return result!;
}
/// Returns true if a [LookupBoundary] is hiding the nearest
/// [RenderObjectWidget] with a [RenderObject] of the specified type `T`
/// from the provided [BuildContext].
///
/// This method throws when asserts are disabled.
static bool debugIsHidingAncestorRenderObjectOfType<T extends RenderObject>(BuildContext context) {
bool? result;
assert(() {
bool hiddenByBoundary = false;
bool ancestorFound = false;
context.visitAncestorElements((Element ancestor) {
if (ancestor is RenderObjectElement && ancestor.renderObject is T) {
ancestorFound = true;
return false;
}
hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
return true;
});
result = ancestorFound & hiddenByBoundary;
return true;
} ());
return result!;
}
@override @override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false; bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
} }
...@@ -28,7 +28,8 @@ void main() { ...@@ -28,7 +28,8 @@ void main() {
error.toStringDeep(), error.toStringDeep(),
'FlutterError\n' 'FlutterError\n'
' No Material widget found.\n' ' No Material widget found.\n'
' Chip widgets require a Material widget ancestor.\n' ' Chip widgets require a Material widget ancestor within the\n'
' closest LookupBoundary.\n'
' In Material Design, most widgets are conceptually "printed" on a\n' ' In Material Design, most widgets are conceptually "printed" on a\n'
" sheet of material. In Flutter's material library, that material\n" " sheet of material. In Flutter's material library, that material\n"
' is represented by the Material widget. It is the Material widget\n' ' is represented by the Material widget. It is the Material widget\n'
......
...@@ -1034,6 +1034,101 @@ void main() { ...@@ -1034,6 +1034,101 @@ void main() {
materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero); materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero);
expect(tracker.paintCount, 2); expect(tracker.paintCount, 2);
}); });
group('LookupBoundary', () {
testWidgets('hides Material from Material.maybeOf', (WidgetTester tester) async {
MaterialInkController? material;
await tester.pumpWidget(
Material(
child: LookupBoundary(
child: Builder(
builder: (BuildContext context) {
material = Material.maybeOf(context);
return Container();
},
),
),
),
);
expect(material, isNull);
});
testWidgets('hides Material from Material.of', (WidgetTester tester) async {
await tester.pumpWidget(
Material(
child: LookupBoundary(
child: Builder(
builder: (BuildContext context) {
Material.of(context);
return Container();
},
),
),
),
);
final Object? exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception! as FlutterError;
expect(
error.toStringDeep(),
'FlutterError\n'
' Material.of() was called with a context that does not have access\n'
' to a Material widget.\n'
' The context provided to Material.of() does have a Material widget\n'
' ancestor, but it is hidden by a LookupBoundary. This can happen\n'
' because you are using a widget that looks for a Material\n'
' ancestor, but no such ancestor exists within the closest\n'
' LookupBoundary.\n'
' The context used was:\n'
' Builder(dirty)\n'
);
});
testWidgets('hides Material from debugCheckHasMaterial', (WidgetTester tester) async {
await tester.pumpWidget(
Material(
child: LookupBoundary(
child: Builder(
builder: (BuildContext context) {
debugCheckHasMaterial(context);
return Container();
},
),
),
),
);
final Object? exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception! as FlutterError;
expect(
error.toStringDeep(), startsWith(
'FlutterError\n'
' No Material widget found within the closest LookupBoundary.\n'
' There is an ancestor Material widget, but it is hidden by a\n'
' LookupBoundary.\n'
' Builder widgets require a Material widget ancestor within the\n'
' closest LookupBoundary.\n'
' In Material Design, most widgets are conceptually "printed" on a\n'
" sheet of material. In Flutter's material library, that material\n"
' is represented by the Material widget. It is the Material widget\n'
' that renders ink splashes, for instance. Because of this, many\n'
' material library widgets require that there be a Material widget\n'
' in the tree above them.\n'
' To introduce a Material widget, you can either directly include\n'
' one, or use a widget that contains Material itself, such as a\n'
' Card, Dialog, Drawer, or Scaffold.\n'
' The specific widget that could not find a Material ancestor was:\n'
' Builder\n'
' The ancestors of this widget were:\n'
' LookupBoundary\n'
),
);
});
});
} }
class TrackPaintInkFeature extends InkFeature { class TrackPaintInkFeature extends InkFeature {
......
...@@ -958,6 +958,130 @@ void main() { ...@@ -958,6 +958,130 @@ void main() {
}); });
}); });
group('LookupBoundary.debugIsHidingAncestorWidgetOfExactType', () {
testWidgets('is hiding', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(Container(
color: Colors.blue,
child: LookupBoundary(
child: Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Container>(context);
return Container();
},
),
),
));
expect(isHidden, isTrue);
});
testWidgets('is not hiding entity within boundary', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(Container(
color: Colors.blue,
child: LookupBoundary(
child: Container(
color: Colors.red,
child: Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Container>(context);
return Container();
},
),
),
),
));
expect(isHidden, isFalse);
});
testWidgets('is not hiding if no boundary exists', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(Container(
color: Colors.blue,
child: Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Container>(context);
return Container();
},
),
));
expect(isHidden, isFalse);
});
testWidgets('is not hiding if no boundary and no entity exists', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Container>(context);
return Container();
},
));
expect(isHidden, isFalse);
});
});
group('LookupBoundary.debugIsHidingAncestorRenderObjectOfType', () {
testWidgets('is hiding', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(Padding(
padding: EdgeInsets.zero,
child: LookupBoundary(
child: Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorRenderObjectOfType<RenderPadding>(context);
return Container();
},
),
),
));
expect(isHidden, isTrue);
});
testWidgets('is not hiding entity within boundary', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(Padding(
padding: EdgeInsets.zero,
child: LookupBoundary(
child: Padding(
padding: EdgeInsets.zero,
child: Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorRenderObjectOfType<RenderPadding>(context);
return Container();
},
),
),
),
));
expect(isHidden, isFalse);
});
testWidgets('is not hiding if no boundary exists', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(Padding(
padding: EdgeInsets.zero,
child: Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorRenderObjectOfType<RenderPadding>(context);
return Container();
},
),
));
expect(isHidden, isFalse);
});
testWidgets('is not hiding if no boundary and no entity exists', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorRenderObjectOfType<RenderPadding>(context);
return Container();
},
));
expect(isHidden, isFalse);
});
});
} }
class MyStatefulContainer extends StatefulWidget { class MyStatefulContainer extends StatefulWidget {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment