Add LookupBoundary to Material (#116736)

This commit is contained in:
Michael Goderbauer 2022-12-09 15:48:00 -08:00 committed by GitHub
parent 332032ddae
commit 9dd30878d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 291 additions and 8 deletions

View file

@ -11,7 +11,8 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger;
// Examples can assume:
// 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
/// only used in contexts where they can print ink onto some material.
@ -32,12 +33,17 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger;
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasMaterial(BuildContext context) {
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>[
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(
'${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 '
"a sheet of material. In Flutter's material library, that "
'material is represented by the Material widget. It is the '

View file

@ -343,7 +343,7 @@ class Material extends StatefulWidget {
final BorderRadiusGeometry? borderRadius;
/// 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:
///
@ -358,11 +358,11 @@ class Material extends StatefulWidget {
/// * [Material.of], which is similar to this method, but asserts if
/// no [Material] ancestor is found.
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 given context.
/// the given context within the closest [LookupBoundary].
///
/// If no [Material] widget ancestor can be found then this method will assert
/// in debug mode, and throw an exception in release mode.
@ -383,6 +383,16 @@ class Material extends StatefulWidget {
final MaterialInkController? controller = maybeOf(context);
assert(() {
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(
'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 '

View file

@ -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
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
}

View file

@ -28,7 +28,8 @@ void main() {
error.toStringDeep(),
'FlutterError\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'
" sheet of material. In Flutter's material library, that material\n"
' is represented by the Material widget. It is the Material widget\n'

View file

@ -1034,6 +1034,101 @@ void main() {
materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero);
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 {

View file

@ -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 {