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:
|
// 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;
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
/// * [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 {
|
|||||||
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 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user