Add LookupBoundary to Material (#116736)
This commit is contained in:
parent
332032ddae
commit
9dd30878d9
@ -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 '
|
||||
|
@ -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 '
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user