Unverified Commit adc5f26b authored by Hans Muller's avatar Hans Muller Committed by GitHub

Re-land ScaffoldMessenger (#65416)

parent c0675577
......@@ -17,6 +17,7 @@ import 'floating_action_button.dart';
import 'icons.dart';
import 'material_localizations.dart';
import 'page.dart';
import 'scaffold.dart';
import 'theme.dart';
/// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage
......@@ -168,6 +169,7 @@ class MaterialApp extends StatefulWidget {
const MaterialApp({
Key key,
this.navigatorKey,
this.scaffoldMessengerKey,
this.home,
this.routes = const <String, WidgetBuilder>{},
this.initialRoute,
......@@ -215,6 +217,7 @@ class MaterialApp extends StatefulWidget {
/// Creates a [MaterialApp] that uses the [Router] instead of a [Navigator].
const MaterialApp.router({
Key key,
this.scaffoldMessengerKey,
this.routeInformationProvider,
@required this.routeInformationParser,
@required this.routerDelegate,
......@@ -263,6 +266,14 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.navigatorKey}
final GlobalKey<NavigatorState> navigatorKey;
/// A key to use when building the [ScaffoldMessenger].
///
/// If a [scaffoldMessengerKey] is specified, the [ScaffoldMessenger] can be
/// directly manipulated without first obtaining it from a [BuildContext] via
/// [ScaffoldMessenger.of]: from the [scaffoldMessengerKey], use the
/// [GlobalKey.currentState] getter.
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey;
/// {@macro flutter.widgets.widgetsApp.home}
final Widget home;
......@@ -722,27 +733,30 @@ class _MaterialAppState extends State<MaterialApp> {
}
theme ??= widget.theme ?? ThemeData.light();
return AnimatedTheme(
data: theme,
isMaterialAppTheme: true,
child: widget.builder != null
? Builder(
builder: (BuildContext context) {
// Why are we surrounding a builder with a builder?
//
// The widget.builder may contain code that invokes
// Theme.of(), which should return the theme we selected
// above in AnimatedTheme. However, if we invoke
// widget.builder() directly as the child of AnimatedTheme
// then there is no Context separating them, and the
// widget.builder() will not find the theme. Therefore, we
// surround widget.builder with yet another builder so that
// a context separates them and Theme.of() correctly
// resolves to the theme we passed to AnimatedTheme.
return widget.builder(context, child);
},
)
: child,
return ScaffoldMessenger(
key: widget.scaffoldMessengerKey,
child: AnimatedTheme(
data: theme,
isMaterialAppTheme: true,
child: widget.builder != null
? Builder(
builder: (BuildContext context) {
// Why are we surrounding a builder with a builder?
//
// The widget.builder may contain code that invokes
// Theme.of(), which should return the theme we selected
// above in AnimatedTheme. However, if we invoke
// widget.builder() directly as the child of AnimatedTheme
// then there is no Context separating them, and the
// widget.builder() will not find the theme. Therefore, we
// surround widget.builder with yet another builder so that
// a context separates them and Theme.of() correctly
// resolves to the theme we passed to AnimatedTheme.
return widget.builder(context, child);
},
)
: child,
)
);
}
......
......@@ -137,7 +137,7 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
/// icon: const Icon(Icons.add_alert),
/// tooltip: 'Show Snackbar',
/// onPressed: () {
/// scaffoldKey.currentState.showSnackBar(snackBar);
/// ScaffoldMessenger.of(context).showSnackBar(snackBar);
/// },
/// ),
/// IconButton(
......
......@@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'scaffold.dart' show Scaffold;
import 'scaffold.dart' show Scaffold, ScaffoldMessenger;
/// Asserts that the given context has a [Material] ancestor.
///
......@@ -125,3 +125,34 @@ bool debugCheckHasScaffold(BuildContext context) {
}());
return true;
}
/// Asserts that the given context has a [ScaffoldMessenger] ancestor.
///
/// Used by various widgets to make sure that they are only used in an
/// appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's build method:
///
/// ```dart
/// assert(debugCheckHasScaffoldMessenger(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasScaffoldMessenger(BuildContext context) {
assert(() {
if (context.widget is! ScaffoldMessenger && context.findAncestorWidgetOfExactType<ScaffoldMessenger>() == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('No ScaffoldMessenger widget found.'),
ErrorDescription('${context.widget.runtimeType} widgets require a ScaffoldMessenger widget ancestor.'),
...context.describeMissingAncestor(expectedAncestorType: ScaffoldMessenger),
ErrorHint(
'Typically, the ScaffoldMessenger widget is introduced by the MaterialApp '
'at the top of your application widget tree.'
)
]);
}
return true;
}());
return true;
}
......@@ -32,7 +32,7 @@ const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlo
/// Specify how a [SnackBar] was closed.
///
/// The [ScaffoldState.showSnackBar] function returns a
/// The [ScaffoldMessengerState.showSnackBar] function returns a
/// [ScaffoldFeatureController]. The value of the controller's closed property
/// is a Future that resolves to a SnackBarClosedReason. Applications that need
/// to know how a snackbar was closed can use this value.
......@@ -40,7 +40,7 @@ const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlo
/// Example:
///
/// ```dart
/// Scaffold.of(context).showSnackBar(
/// ScaffoldMessenger.of(context).showSnackBar(
/// SnackBar( ... )
/// ).closed.then((SnackBarClosedReason reason) {
/// ...
......@@ -57,10 +57,10 @@ enum SnackBarClosedReason {
swipe,
/// The snack bar was closed by the [ScaffoldFeatureController] close callback
/// or by calling [ScaffoldState.hideCurrentSnackBar] directly.
/// or by calling [ScaffoldMessengerState.hideCurrentSnackBar] directly.
hide,
/// The snack bar was closed by an call to [ScaffoldState.removeCurrentSnackBar].
/// The snack bar was closed by an call to [ScaffoldMessengerState.removeCurrentSnackBar].
remove,
/// The snack bar was closed because its timer expired.
......@@ -123,7 +123,7 @@ class _SnackBarActionState extends State<SnackBarAction> {
_haveTriggeredAction = true;
});
widget.onPressed();
Scaffold.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action);
ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action);
}
@override
......@@ -146,8 +146,8 @@ class _SnackBarActionState extends State<SnackBarAction> {
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=zpO6n_oZWw0}
///
/// To display a snack bar, call `Scaffold.of(context).showSnackBar()`, passing
/// an instance of [SnackBar] that describes the message.
/// To display a snack bar, call `ScaffoldMessenger.of(context).showSnackBar()`,
/// passing an instance of [SnackBar] that describes the message.
///
/// To control how long the [SnackBar] remains visible, specify a [duration].
///
......@@ -156,11 +156,11 @@ class _SnackBarActionState extends State<SnackBarAction> {
///
/// See also:
///
/// * [Scaffold.of], to obtain the current [ScaffoldState], which manages the
/// display and animation of snack bars.
/// * [ScaffoldState.showSnackBar], which displays a [SnackBar].
/// * [ScaffoldState.removeCurrentSnackBar], which abruptly hides the currently
/// displayed snack bar, if any, and allows the next to be displayed.
/// * [ScaffoldMessenger.of], to obtain the current [ScaffoldMessengerState],
/// which manages the display and animation of snack bars.
/// * [ScaffoldMessengerState.showSnackBar], which displays a [SnackBar].
/// * [ScaffoldMessengerState.removeCurrentSnackBar], which abruptly hides the
/// currently displayed snack bar, if any, and allows the next to be displayed.
/// * [SnackBarAction], which is used to specify an [action] button to show
/// on the snack bar.
/// * [SnackBarThemeData], to configure the default property values for
......@@ -289,7 +289,7 @@ class SnackBar extends StatefulWidget {
///
/// See also:
///
/// * [ScaffoldState.removeCurrentSnackBar], which abruptly hides the
/// * [ScaffoldMessengerState.removeCurrentSnackBar], which abruptly hides the
/// currently displayed snack bar, if any, and allows the next to be
/// displayed.
/// * <https://material.io/design/components/snackbars.html>
......@@ -301,7 +301,7 @@ class SnackBar extends StatefulWidget {
/// Called the first time that the snackbar is visible within a [Scaffold].
final VoidCallback onVisible;
// API for Scaffold.showSnackBar():
// API for ScaffoldMessengerState.showSnackBar():
/// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
static AnimationController createAnimationController({ @required TickerProvider vsync }) {
......@@ -516,14 +516,14 @@ class _SnackBarState extends State<SnackBar> {
container: true,
liveRegion: true,
onDismiss: () {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
},
child: Dismissible(
key: const Key('dismissible'),
direction: DismissDirection.down,
resizeDuration: null,
onDismissed: (DismissDirection direction) {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
},
child: snackBar,
),
......@@ -550,7 +550,9 @@ class _SnackBarState extends State<SnackBar> {
child: snackBar,
);
}
return ClipRect(child: snackBarTransition);
return Hero(
child: ClipRect(child: snackBarTransition),
tag: '<SnackBar Hero tag - ${widget.content}>',
);
}
}
......@@ -512,6 +512,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// class _SliverAnimatedListSampleState extends State<SliverAnimatedListSample> {
/// final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
/// final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
/// final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// ListModel<int> _list;
/// int _selectedItem;
/// int _nextItem; // The next item inserted when the user presses the '+' button.
......@@ -569,7 +570,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// _selectedItem = null;
/// });
/// } else {
/// _scaffoldKey.currentState.showSnackBar(SnackBar(
/// _scaffoldMessengerKey.currentState.showSnackBar(SnackBar(
/// content: Text(
/// 'Select an item to remove from the list.',
/// style: TextStyle(fontSize: 20),
......@@ -581,6 +582,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// scaffoldMessengerKey: _scaffoldMessengerKey,
/// home: Scaffold(
/// key: _scaffoldKey,
/// body: CustomScrollView(
......
......@@ -2084,33 +2084,52 @@ typedef ElementVisitor = void Function(Element element);
/// widget can be used: the build context passed to the [Builder.builder]
/// callback will be that of the [Builder] itself.
///
/// For example, in the following snippet, the [ScaffoldState.showSnackBar]
/// For example, in the following snippet, the [ScaffoldState.showBottomSheet]
/// method is called on the [Scaffold] widget that the build method itself
/// creates. If a [Builder] had not been used, and instead the `context`
/// argument of the build method itself had been used, no [Scaffold] would have
/// been found, and the [Scaffold.of] function would have returned null.
///
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// // here, Scaffold.of(context) returns null
/// return Scaffold(
/// appBar: AppBar(title: Text('Demo')),
/// body: Builder(
/// builder: (BuildContext context) {
/// return TextButton(
/// child: Text('BUTTON'),
/// onPressed: () {
/// // here, Scaffold.of(context) returns the locally created Scaffold
/// Scaffold.of(context).showSnackBar(SnackBar(
/// content: Text('Hello.')
/// ));
/// }
/// );
/// }
/// )
/// );
/// }
/// @override
/// Widget build(BuildContext context) {
/// // here, Scaffold.of(context) returns null
/// return Scaffold(
/// appBar: AppBar(title: Text('Demo')),
/// body: Builder(
/// builder: (BuildContext context) {
/// return TextButton(
/// child: Text('BUTTON'),
/// onPressed: () {
/// Scaffold.of(context).showBottomSheet<void>(
/// (BuildContext context) {
/// return Container(
/// alignment: Alignment.center,
/// height: 200,
/// color: Colors.amber,
/// child: Center(
/// child: Column(
/// mainAxisSize: MainAxisSize.min,
/// children: <Widget>[
/// const Text('BottomSheet'),
/// ElevatedButton(
/// child: const Text('Close BottomSheet'),
/// onPressed: () {
/// Navigator.pop(context),
/// },
/// )
/// ],
/// ),
/// ),
/// );
/// },
/// );
/// },
/// );
/// },
/// )
/// );
/// }
/// ```
///
/// The [BuildContext] for a particular widget can change location over time as
......
......@@ -164,6 +164,8 @@ void main() {
' _InheritedTheme\n'
' Theme\n'
' AnimatedTheme\n'
' _ScaffoldMessengerScope\n'
' ScaffoldMessenger\n'
' Builder\n'
' DefaultTextStyle\n'
' CustomPaint\n'
......@@ -196,4 +198,50 @@ void main() {
' or WidgetsApp widget at the top of your application widget tree.\n',
));
});
testWidgets('debugCheckHasScaffoldMessenger control test', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
key: _scaffoldKey,
body: Container(),
),
),
));
FlutterError error;
try {
_scaffoldKey.currentState.showSnackBar(const SnackBar(content: Text('Something is missing here')));
} on FlutterError catch (e) {
error = e;
} finally {
expect(error.diagnostics.length, 5);
expect(error.diagnostics[2], isA<DiagnosticsProperty<Element>>());
expect(error.diagnostics[3], isA<DiagnosticsBlock>());
expect(error.diagnostics[4].level, DiagnosticLevel.hint);
expect(
error.diagnostics[4].toStringDeep(),
equalsIgnoringHashCodes(
'Typically, the ScaffoldMessenger widget is introduced by the\n'
'MaterialApp at the top of your application widget tree.\n',
),
);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' No ScaffoldMessenger widget found.\n'
' Scaffold widgets require a ScaffoldMessenger widget ancestor.\n'
' The specific widget that could not find a ScaffoldMessenger\n'
' ancestor was:\n'
' Scaffold-[LabeledGlobalKey<ScaffoldState>#d60fa]\n'
' The ancestors of this widget were:\n'
' MediaQuery\n'
' Directionality\n'
' [root]\n'
' Typically, the ScaffoldMessenger widget is introduced by the\n'
' MaterialApp at the top of your application widget tree.\n'
));
}
});
}
......@@ -649,7 +649,7 @@ void main() {
builder: (BuildContext context) {
return FloatingActionButton(
onPressed: () {
Scaffold.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Snacky!')),
);
},
......
......@@ -73,7 +73,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text(text),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -110,7 +110,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text(text),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -153,7 +153,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
......@@ -200,7 +200,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -242,7 +242,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......
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