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'; ...@@ -17,6 +17,7 @@ import 'floating_action_button.dart';
import 'icons.dart'; import 'icons.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'page.dart'; import 'page.dart';
import 'scaffold.dart';
import 'theme.dart'; import 'theme.dart';
/// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage /// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage
...@@ -168,6 +169,7 @@ class MaterialApp extends StatefulWidget { ...@@ -168,6 +169,7 @@ class MaterialApp extends StatefulWidget {
const MaterialApp({ const MaterialApp({
Key key, Key key,
this.navigatorKey, this.navigatorKey,
this.scaffoldMessengerKey,
this.home, this.home,
this.routes = const <String, WidgetBuilder>{}, this.routes = const <String, WidgetBuilder>{},
this.initialRoute, this.initialRoute,
...@@ -215,6 +217,7 @@ class MaterialApp extends StatefulWidget { ...@@ -215,6 +217,7 @@ class MaterialApp extends StatefulWidget {
/// Creates a [MaterialApp] that uses the [Router] instead of a [Navigator]. /// Creates a [MaterialApp] that uses the [Router] instead of a [Navigator].
const MaterialApp.router({ const MaterialApp.router({
Key key, Key key,
this.scaffoldMessengerKey,
this.routeInformationProvider, this.routeInformationProvider,
@required this.routeInformationParser, @required this.routeInformationParser,
@required this.routerDelegate, @required this.routerDelegate,
...@@ -263,6 +266,14 @@ class MaterialApp extends StatefulWidget { ...@@ -263,6 +266,14 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.navigatorKey} /// {@macro flutter.widgets.widgetsApp.navigatorKey}
final GlobalKey<NavigatorState> 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} /// {@macro flutter.widgets.widgetsApp.home}
final Widget home; final Widget home;
...@@ -722,7 +733,9 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -722,7 +733,9 @@ class _MaterialAppState extends State<MaterialApp> {
} }
theme ??= widget.theme ?? ThemeData.light(); theme ??= widget.theme ?? ThemeData.light();
return AnimatedTheme( return ScaffoldMessenger(
key: widget.scaffoldMessengerKey,
child: AnimatedTheme(
data: theme, data: theme,
isMaterialAppTheme: true, isMaterialAppTheme: true,
child: widget.builder != null child: widget.builder != null
...@@ -743,6 +756,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -743,6 +756,7 @@ class _MaterialAppState extends State<MaterialApp> {
}, },
) )
: child, : child,
)
); );
} }
......
...@@ -137,7 +137,7 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate { ...@@ -137,7 +137,7 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
/// icon: const Icon(Icons.add_alert), /// icon: const Icon(Icons.add_alert),
/// tooltip: 'Show Snackbar', /// tooltip: 'Show Snackbar',
/// onPressed: () { /// onPressed: () {
/// scaffoldKey.currentState.showSnackBar(snackBar); /// ScaffoldMessenger.of(context).showSnackBar(snackBar);
/// }, /// },
/// ), /// ),
/// IconButton( /// IconButton(
......
...@@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart'; ...@@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart';
import 'material.dart'; import 'material.dart';
import 'material_localizations.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. /// Asserts that the given context has a [Material] ancestor.
/// ///
...@@ -125,3 +125,34 @@ bool debugCheckHasScaffold(BuildContext context) { ...@@ -125,3 +125,34 @@ bool debugCheckHasScaffold(BuildContext context) {
}()); }());
return true; 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 ...@@ -32,7 +32,7 @@ const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlo
/// Specify how a [SnackBar] was closed. /// 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 /// [ScaffoldFeatureController]. The value of the controller's closed property
/// is a Future that resolves to a SnackBarClosedReason. Applications that need /// is a Future that resolves to a SnackBarClosedReason. Applications that need
/// to know how a snackbar was closed can use this value. /// 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 ...@@ -40,7 +40,7 @@ const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlo
/// Example: /// Example:
/// ///
/// ```dart /// ```dart
/// Scaffold.of(context).showSnackBar( /// ScaffoldMessenger.of(context).showSnackBar(
/// SnackBar( ... ) /// SnackBar( ... )
/// ).closed.then((SnackBarClosedReason reason) { /// ).closed.then((SnackBarClosedReason reason) {
/// ... /// ...
...@@ -57,10 +57,10 @@ enum SnackBarClosedReason { ...@@ -57,10 +57,10 @@ enum SnackBarClosedReason {
swipe, swipe,
/// The snack bar was closed by the [ScaffoldFeatureController] close callback /// The snack bar was closed by the [ScaffoldFeatureController] close callback
/// or by calling [ScaffoldState.hideCurrentSnackBar] directly. /// or by calling [ScaffoldMessengerState.hideCurrentSnackBar] directly.
hide, hide,
/// The snack bar was closed by an call to [ScaffoldState.removeCurrentSnackBar]. /// The snack bar was closed by an call to [ScaffoldMessengerState.removeCurrentSnackBar].
remove, remove,
/// The snack bar was closed because its timer expired. /// The snack bar was closed because its timer expired.
...@@ -123,7 +123,7 @@ class _SnackBarActionState extends State<SnackBarAction> { ...@@ -123,7 +123,7 @@ class _SnackBarActionState extends State<SnackBarAction> {
_haveTriggeredAction = true; _haveTriggeredAction = true;
}); });
widget.onPressed(); widget.onPressed();
Scaffold.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action); ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action);
} }
@override @override
...@@ -146,8 +146,8 @@ class _SnackBarActionState extends State<SnackBarAction> { ...@@ -146,8 +146,8 @@ class _SnackBarActionState extends State<SnackBarAction> {
/// ///
/// {@youtube 560 315 https://www.youtube.com/watch?v=zpO6n_oZWw0} /// {@youtube 560 315 https://www.youtube.com/watch?v=zpO6n_oZWw0}
/// ///
/// To display a snack bar, call `Scaffold.of(context).showSnackBar()`, passing /// To display a snack bar, call `ScaffoldMessenger.of(context).showSnackBar()`,
/// an instance of [SnackBar] that describes the message. /// passing an instance of [SnackBar] that describes the message.
/// ///
/// To control how long the [SnackBar] remains visible, specify a [duration]. /// To control how long the [SnackBar] remains visible, specify a [duration].
/// ///
...@@ -156,11 +156,11 @@ class _SnackBarActionState extends State<SnackBarAction> { ...@@ -156,11 +156,11 @@ class _SnackBarActionState extends State<SnackBarAction> {
/// ///
/// See also: /// See also:
/// ///
/// * [Scaffold.of], to obtain the current [ScaffoldState], which manages the /// * [ScaffoldMessenger.of], to obtain the current [ScaffoldMessengerState],
/// display and animation of snack bars. /// which manages the display and animation of snack bars.
/// * [ScaffoldState.showSnackBar], which displays a [SnackBar]. /// * [ScaffoldMessengerState.showSnackBar], which displays a [SnackBar].
/// * [ScaffoldState.removeCurrentSnackBar], which abruptly hides the currently /// * [ScaffoldMessengerState.removeCurrentSnackBar], which abruptly hides the
/// displayed snack bar, if any, and allows the next to be displayed. /// currently displayed snack bar, if any, and allows the next to be displayed.
/// * [SnackBarAction], which is used to specify an [action] button to show /// * [SnackBarAction], which is used to specify an [action] button to show
/// on the snack bar. /// on the snack bar.
/// * [SnackBarThemeData], to configure the default property values for /// * [SnackBarThemeData], to configure the default property values for
...@@ -289,7 +289,7 @@ class SnackBar extends StatefulWidget { ...@@ -289,7 +289,7 @@ class SnackBar extends StatefulWidget {
/// ///
/// See also: /// 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 /// currently displayed snack bar, if any, and allows the next to be
/// displayed. /// displayed.
/// * <https://material.io/design/components/snackbars.html> /// * <https://material.io/design/components/snackbars.html>
...@@ -301,7 +301,7 @@ class SnackBar extends StatefulWidget { ...@@ -301,7 +301,7 @@ class SnackBar extends StatefulWidget {
/// Called the first time that the snackbar is visible within a [Scaffold]. /// Called the first time that the snackbar is visible within a [Scaffold].
final VoidCallback onVisible; 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. /// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
static AnimationController createAnimationController({ @required TickerProvider vsync }) { static AnimationController createAnimationController({ @required TickerProvider vsync }) {
...@@ -516,14 +516,14 @@ class _SnackBarState extends State<SnackBar> { ...@@ -516,14 +516,14 @@ class _SnackBarState extends State<SnackBar> {
container: true, container: true,
liveRegion: true, liveRegion: true,
onDismiss: () { onDismiss: () {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss); ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
}, },
child: Dismissible( child: Dismissible(
key: const Key('dismissible'), key: const Key('dismissible'),
direction: DismissDirection.down, direction: DismissDirection.down,
resizeDuration: null, resizeDuration: null,
onDismissed: (DismissDirection direction) { onDismissed: (DismissDirection direction) {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe); ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
}, },
child: snackBar, child: snackBar,
), ),
...@@ -550,7 +550,9 @@ class _SnackBarState extends State<SnackBar> { ...@@ -550,7 +550,9 @@ class _SnackBarState extends State<SnackBar> {
child: snackBar, child: snackBar,
); );
} }
return Hero(
return ClipRect(child: snackBarTransition); child: ClipRect(child: snackBarTransition),
tag: '<SnackBar Hero tag - ${widget.content}>',
);
} }
} }
...@@ -512,6 +512,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -512,6 +512,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// class _SliverAnimatedListSampleState extends State<SliverAnimatedListSample> { /// class _SliverAnimatedListSampleState extends State<SliverAnimatedListSample> {
/// final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>(); /// final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
/// final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); /// final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
/// final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// ListModel<int> _list; /// ListModel<int> _list;
/// int _selectedItem; /// int _selectedItem;
/// int _nextItem; // The next item inserted when the user presses the '+' button. /// int _nextItem; // The next item inserted when the user presses the '+' button.
...@@ -569,7 +570,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -569,7 +570,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// _selectedItem = null; /// _selectedItem = null;
/// }); /// });
/// } else { /// } else {
/// _scaffoldKey.currentState.showSnackBar(SnackBar( /// _scaffoldMessengerKey.currentState.showSnackBar(SnackBar(
/// content: Text( /// content: Text(
/// 'Select an item to remove from the list.', /// 'Select an item to remove from the list.',
/// style: TextStyle(fontSize: 20), /// style: TextStyle(fontSize: 20),
...@@ -581,6 +582,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -581,6 +582,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// @override /// @override
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return MaterialApp( /// return MaterialApp(
/// scaffoldMessengerKey: _scaffoldMessengerKey,
/// home: Scaffold( /// home: Scaffold(
/// key: _scaffoldKey, /// key: _scaffoldKey,
/// body: CustomScrollView( /// body: CustomScrollView(
......
...@@ -2084,7 +2084,7 @@ typedef ElementVisitor = void Function(Element element); ...@@ -2084,7 +2084,7 @@ typedef ElementVisitor = void Function(Element element);
/// widget can be used: the build context passed to the [Builder.builder] /// widget can be used: the build context passed to the [Builder.builder]
/// callback will be that of the [Builder] itself. /// 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 /// method is called on the [Scaffold] widget that the build method itself
/// creates. If a [Builder] had not been used, and instead the `context` /// 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 /// argument of the build method itself had been used, no [Scaffold] would have
...@@ -2101,13 +2101,32 @@ typedef ElementVisitor = void Function(Element element); ...@@ -2101,13 +2101,32 @@ typedef ElementVisitor = void Function(Element element);
/// return TextButton( /// return TextButton(
/// child: Text('BUTTON'), /// child: Text('BUTTON'),
/// onPressed: () { /// onPressed: () {
/// // here, Scaffold.of(context) returns the locally created Scaffold /// Scaffold.of(context).showBottomSheet<void>(
/// Scaffold.of(context).showSnackBar(SnackBar( /// (BuildContext context) {
/// content: Text('Hello.') /// 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),
/// },
/// )
/// ],
/// ),
/// ),
/// ); /// );
/// } /// },
/// );
/// },
/// );
/// },
/// ) /// )
/// ); /// );
/// } /// }
......
...@@ -164,6 +164,8 @@ void main() { ...@@ -164,6 +164,8 @@ void main() {
' _InheritedTheme\n' ' _InheritedTheme\n'
' Theme\n' ' Theme\n'
' AnimatedTheme\n' ' AnimatedTheme\n'
' _ScaffoldMessengerScope\n'
' ScaffoldMessenger\n'
' Builder\n' ' Builder\n'
' DefaultTextStyle\n' ' DefaultTextStyle\n'
' CustomPaint\n' ' CustomPaint\n'
...@@ -196,4 +198,50 @@ void main() { ...@@ -196,4 +198,50 @@ void main() {
' or WidgetsApp widget at the top of your application widget tree.\n', ' 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() { ...@@ -649,7 +649,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return FloatingActionButton( return FloatingActionButton(
onPressed: () { onPressed: () {
Scaffold.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Snacky!')), const SnackBar(content: Text('Snacky!')),
); );
}, },
......
...@@ -73,7 +73,7 @@ void main() { ...@@ -73,7 +73,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text(text), content: const Text(text),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}), action: SnackBarAction(label: 'ACTION', onPressed: () {}),
...@@ -110,7 +110,7 @@ void main() { ...@@ -110,7 +110,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text(text), content: const Text(text),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}), action: SnackBarAction(label: 'ACTION', onPressed: () {}),
...@@ -153,7 +153,7 @@ void main() { ...@@ -153,7 +153,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
elevation: elevation, elevation: elevation,
shape: shape, shape: shape,
...@@ -200,7 +200,7 @@ void main() { ...@@ -200,7 +200,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'), content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}), action: SnackBarAction(label: 'ACTION', onPressed: () {}),
...@@ -242,7 +242,7 @@ void main() { ...@@ -242,7 +242,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'), content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}), 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