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;
}
...@@ -19,6 +19,7 @@ import 'bottom_sheet.dart'; ...@@ -19,6 +19,7 @@ import 'bottom_sheet.dart';
import 'button_bar.dart'; import 'button_bar.dart';
import 'colors.dart'; import 'colors.dart';
import 'curves.dart'; import 'curves.dart';
import 'debug.dart';
import 'divider.dart'; import 'divider.dart';
import 'drawer.dart'; import 'drawer.dart';
import 'flexible_space_bar.dart'; import 'flexible_space_bar.dart';
...@@ -61,6 +62,372 @@ enum _ScaffoldSlot { ...@@ -61,6 +62,372 @@ enum _ScaffoldSlot {
statusBar, statusBar,
} }
/// Manages [SnackBar]s for descendant [Scaffold]s.
///
/// This class provides APIs for showing snack bars.
///
/// To display a snack bar, obtain the [ScaffoldMessengerState] for the current
/// [BuildContext] via [ScaffoldMessenger.of] and use the
/// [ScaffoldMessengerState.showSnackBar] function.
///
/// See also:
///
/// * [SnackBar], which is a temporary notification typically shown near the
/// bottom of the app using the [ScaffoldMessengerState.showSnackBar] method.
/// * Cookbook: [Display a snackbar](https://flutter.dev/docs/cookbook/design/snackbars)
class ScaffoldMessenger extends StatefulWidget {
/// Creates a widget that manages [SnackBar]s for [Scaffold] descendants.
const ScaffoldMessenger({
Key key,
@required this.child,
}) : assert(child != null),
super(key: key);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.child}
final Widget child;
/// The state from the closest instance of this class that encloses the given
/// context.
///
/// {@tool dartpad --template=stateless_widget_scaffold_center}
/// Typical usage of the [ScaffoldMessenger.of] function is to call it in
/// response to a user gesture or an application state change.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return ElevatedButton(
/// child: const Text('SHOW A SNACKBAR'),
/// onPressed: () {
/// ScaffoldMessenger.of(context).showSnackBar(
/// const SnackBar(
/// content: Text('Have a snack!'),
/// ),
/// );
/// },
/// );
/// }
/// ```
/// {@end-tool}
///
/// A less elegant but more expedient solution is assign a [GlobalKey] to the
/// [ScaffoldMessenger], then use the `key.currentState` property to obtain the
/// [ScaffoldMessengerState] rather than using the [ScaffoldMessenger.of]
/// function. The [MaterialApp.scaffoldMessengerKey] refers to the root
/// ScaffoldMessenger that is provided by default.
///
/// {@tool dartpad --template=freeform}
/// Sometimes [SnackBar]s are produced by code that doesn't have ready access
/// to a valid [BuildContext]. One such example of this is when you may want
/// to show a SnackBar from a method outside of the `build` function. In these
/// cases, you can assign a [GlobalKey] to the [ScaffoldMessenger]. This
/// example shows a key being used to obtain the [ScaffoldMessengerState]
/// provided by the [MaterialApp].
///
/// ```dart imports
/// import 'package:flutter/material.dart';
/// ```
/// ```dart
/// void main() => runApp(MyApp());
///
/// class MyApp extends StatefulWidget {
/// @override
/// _MyAppState createState() => _MyAppState();
/// }
///
/// class _MyAppState extends State<MyApp> {
/// final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// int _counter = 0;
///
/// void _incrementCounter() {
/// setState(() {
/// _counter++;
/// });
/// if (_counter % 10 == 0) {
/// _scaffoldMessengerKey.currentState.showSnackBar(const SnackBar(
/// content: Text('A multiple of ten!'),
/// ));
/// }
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// scaffoldMessengerKey: _scaffoldMessengerKey,
/// home: Scaffold(
/// appBar: AppBar(title: Text('ScaffoldMessenger Demo')),
/// body: Center(
/// child: Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: <Widget>[
/// Text(
/// 'You have pushed the button this many times:',
/// ),
/// Text(
/// '$_counter',
/// style: Theme.of(context).textTheme.headline4,
/// ),
/// ],
/// ),
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: _incrementCounter,
/// tooltip: 'Increment',
/// child: Icon(Icons.add),
/// ),
/// ),
/// );
/// }
/// }
///
/// ```
/// {@end-tool}
///
/// If there is no [ScaffoldMessenger] in scope, then this will throw an
/// exception.
static ScaffoldMessengerState of(BuildContext context) {
assert(context != null);
final _ScaffoldMessengerScope scope = context.dependOnInheritedWidgetOfExactType<_ScaffoldMessengerScope>();
return scope?._scaffoldMessengerState;
}
@override
ScaffoldMessengerState createState() => ScaffoldMessengerState();
}
/// State for a [ScaffoldMessenger].
///
/// A [ScaffoldMessengerState] object can be used to [showSnackBar] for every
/// registered [Scaffold] that is a descendant of the associated
/// [ScaffoldMessenger]. Scaffolds will register to receive [SnackBar]s from
/// their closest ScaffoldMessenger ancestor.
///
/// Typically obtained via [ScaffoldMessenger.of].
class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProviderStateMixin {
final LinkedHashSet<ScaffoldState> _scaffolds = LinkedHashSet<ScaffoldState>();
final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
AnimationController _snackBarController;
Timer _snackBarTimer;
bool _accessibleNavigation;
@override
void didChangeDependencies() {
final MediaQueryData mediaQuery = MediaQuery.of(context);
// If we transition from accessible navigation to non-accessible navigation
// and there is a SnackBar that would have timed out that has already
// completed its timer, dismiss that SnackBar. If the timer hasn't finished
// yet, let it timeout as normal.
if (_accessibleNavigation == true
&& !mediaQuery.accessibleNavigation
&& _snackBarTimer != null
&& !_snackBarTimer.isActive) {
hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
}
_accessibleNavigation = mediaQuery.accessibleNavigation;
super.didChangeDependencies();
}
void _register(ScaffoldState scaffold) {
_scaffolds.add(scaffold);
if (_snackBars.isNotEmpty) {
scaffold._updateSnackBar();
}
}
void _unregister(ScaffoldState scaffold) {
final bool removed = _scaffolds.remove(scaffold);
// ScaffoldStates should only be removed once.
assert(removed);
}
/// Shows a [SnackBar] across all registered [Scaffold]s.
///
/// A scaffold can show at most one snack bar at a time. If this function is
/// called while another snack bar is already visible, the given snack bar
/// will be added to a queue and displayed after the earlier snack bars have
/// closed.
///
/// To control how long a [SnackBar] remains visible, use [SnackBar.duration].
///
/// To remove the [SnackBar] with an exit animation, use [hideCurrentSnackBar]
/// or call [ScaffoldFeatureController.close] on the returned
/// [ScaffoldFeatureController]. To remove a [SnackBar] suddenly (without an
/// animation), use [removeCurrentSnackBar].
///
/// See [ScaffoldMessenger.of] for information about how to obtain the
/// [ScaffoldMessengerState].
///
/// {@tool dartpad --template=stateless_widget_scaffold_center}
///
/// Here is an example of showing a [SnackBar] when the user presses a button.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return OutlinedButton(
/// onPressed: () {
/// ScaffoldMessenger.of(context).showSnackBar(
/// const SnackBar(
/// content: Text('A SnackBar has been shown.'),
/// ),
/// );
/// },
/// child: const Text('Show SnackBar'),
/// );
/// }
/// ```
/// {@end-tool}
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackBar) {
_snackBarController ??= SnackBar.createAnimationController(vsync: this)
..addStatusListener(_handleStatusChanged);
if (_snackBars.isEmpty) {
assert(_snackBarController.isDismissed);
_snackBarController.forward();
}
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> controller;
controller = ScaffoldFeatureController<SnackBar, SnackBarClosedReason>._(
// We provide a fallback key so that if back-to-back snackbars happen to
// match in structure, material ink splashes and highlights don't survive
// from one to the next.
snackBar.withAnimation(_snackBarController, fallbackKey: UniqueKey()),
Completer<SnackBarClosedReason>(),
() {
assert(_snackBars.first == controller);
hideCurrentSnackBar(reason: SnackBarClosedReason.hide);
},
null, // SnackBar doesn't use a builder function so setState() wouldn't rebuild it
);
setState(() {
_snackBars.addLast(controller);
});
_updateScaffolds();
return controller;
}
void _handleStatusChanged(AnimationStatus status) {
switch (status) {
case AnimationStatus.dismissed:
assert(_snackBars.isNotEmpty);
setState(() {
_snackBars.removeFirst();
});
_updateScaffolds();
if (_snackBars.isNotEmpty) {
_snackBarController.forward();
}
break;
case AnimationStatus.completed:
setState(() {
assert(_snackBarTimer == null);
// build will create a new timer if necessary to dismiss the snackBar.
});
_updateScaffolds();
break;
case AnimationStatus.forward:
break;
case AnimationStatus.reverse:
break;
}
}
void _updateScaffolds() {
for (final ScaffoldState scaffold in _scaffolds) {
scaffold._updateSnackBar();
}
}
/// Removes the current [SnackBar] (if any) immediately from registered
/// [Scaffold]s.
///
/// The removed snack bar does not run its normal exit animation. If there are
/// any queued snack bars, they begin their entrance animation immediately.
void removeCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.remove }) {
assert(reason != null);
if (_snackBars.isEmpty)
return;
final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
if (!completer.isCompleted)
completer.complete(reason);
_snackBarTimer?.cancel();
_snackBarTimer = null;
// This will trigger the animation's status callback.
_snackBarController.value = 0.0;
}
/// Removes the current [SnackBar] by running its normal exit animation.
///
/// The closed completer is called after the animation is complete.
void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) {
assert(reason != null);
if (_snackBars.isEmpty || _snackBarController.status == AnimationStatus.dismissed)
return;
final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
if (_accessibleNavigation) {
_snackBarController.value = 0.0;
completer.complete(reason);
} else {
_snackBarController.reverse().then<void>((void value) {
assert(mounted);
if (!completer.isCompleted)
completer.complete(reason);
});
}
_snackBarTimer?.cancel();
_snackBarTimer = null;
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
final MediaQueryData mediaQuery = MediaQuery.of(context);
_accessibleNavigation = mediaQuery.accessibleNavigation;
if (_snackBars.isNotEmpty) {
final ModalRoute<dynamic> route = ModalRoute.of(context);
if (route == null || route.isCurrent) {
if (_snackBarController.isCompleted && _snackBarTimer == null) {
final SnackBar snackBar = _snackBars.first._widget;
_snackBarTimer = Timer(snackBar.duration, () {
assert(_snackBarController.status == AnimationStatus.forward || _snackBarController.status == AnimationStatus.completed);
// Look up MediaQuery again in case the setting changed.
final MediaQueryData mediaQuery = MediaQuery.of(context);
if (mediaQuery.accessibleNavigation && snackBar.action != null)
return;
hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
});
}
}
}
return _ScaffoldMessengerScope(
scaffoldMessengerState: this,
child: widget.child,
);
}
@override
void dispose() {
_snackBarController?.dispose();
_snackBarTimer?.cancel();
_snackBarTimer = null;
super.dispose();
}
}
class _ScaffoldMessengerScope extends InheritedWidget {
const _ScaffoldMessengerScope({
Key key,
Widget child,
ScaffoldMessengerState scaffoldMessengerState,
}) : _scaffoldMessengerState = scaffoldMessengerState,
super(key: key, child: child);
final ScaffoldMessengerState _scaffoldMessengerState;
@override
bool updateShouldNotify(_ScaffoldMessengerScope old) => _scaffoldMessengerState != old._scaffoldMessengerState;
}
/// The geometry of the [Scaffold] after all its contents have been laid out /// The geometry of the [Scaffold] after all its contents have been laid out
/// except the [FloatingActionButton]. /// except the [FloatingActionButton].
/// ///
...@@ -837,11 +1204,11 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -837,11 +1204,11 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// Implements the basic material design visual layout structure. /// Implements the basic material design visual layout structure.
/// ///
/// This class provides APIs for showing drawers, snack bars, and bottom sheets. /// This class provides APIs for showing drawers and bottom sheets.
/// ///
/// To display a snackbar or a persistent bottom sheet, obtain the /// To display a persistent bottom sheet, obtain the
/// [ScaffoldState] for the current [BuildContext] via [Scaffold.of] and use the /// [ScaffoldState] for the current [BuildContext] via [Scaffold.of] and use the
/// [ScaffoldState.showSnackBar] and [ScaffoldState.showBottomSheet] functions. /// [ScaffoldState.showBottomSheet] function.
/// ///
/// {@tool dartpad --template=stateful_widget_material} /// {@tool dartpad --template=stateful_widget_material}
/// This example shows a [Scaffold] with a [body] and [FloatingActionButton]. /// This example shows a [Scaffold] with a [body] and [FloatingActionButton].
...@@ -918,7 +1285,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -918,7 +1285,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return Scaffold( /// return Scaffold(
/// appBar: AppBar( /// appBar: AppBar(
/// title: Text('Sample Code'), /// title: const Text('Sample Code'),
/// ), /// ),
/// body: Center( /// body: Center(
/// child: Text('You have pressed the button $_count times.'), /// child: Text('You have pressed the button $_count times.'),
...@@ -1006,8 +1373,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -1006,8 +1373,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// * [BottomNavigationBar], which is a horizontal array of buttons typically /// * [BottomNavigationBar], which is a horizontal array of buttons typically
/// shown along the bottom of the app using the [bottomNavigationBar] /// shown along the bottom of the app using the [bottomNavigationBar]
/// property. /// property.
/// * [SnackBar], which is a temporary notification typically shown near the
/// bottom of the app using the [ScaffoldState.showSnackBar] method.
/// * [BottomSheet], which is an overlay typically shown near the bottom of the /// * [BottomSheet], which is an overlay typically shown near the bottom of the
/// app. A bottom sheet can either be persistent, in which case it is shown /// app. A bottom sheet can either be persistent, in which case it is shown
/// using the [ScaffoldState.showBottomSheet] method, or modal, in which case /// using the [ScaffoldState.showBottomSheet] method, or modal, in which case
...@@ -1015,7 +1380,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -1015,7 +1380,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// * [ScaffoldState], which is the state associated with this widget. /// * [ScaffoldState], which is the state associated with this widget.
/// * <https://material.io/design/layout/responsive-layout-grid.html> /// * <https://material.io/design/layout/responsive-layout-grid.html>
/// * Cookbook: [Add a Drawer to a screen](https://flutter.dev/docs/cookbook/design/drawer) /// * Cookbook: [Add a Drawer to a screen](https://flutter.dev/docs/cookbook/design/drawer)
/// * Cookbook: [Display a snackbar](https://flutter.dev/docs/cookbook/design/snackbars)
/// * See our /// * See our
/// [Scaffold Sample Apps](https://flutter.dev/docs/catalog/samples/Scaffold). /// [Scaffold Sample Apps](https://flutter.dev/docs/catalog/samples/Scaffold).
class Scaffold extends StatefulWidget { class Scaffold extends StatefulWidget {
...@@ -1387,7 +1751,7 @@ class Scaffold extends StatefulWidget { ...@@ -1387,7 +1751,7 @@ class Scaffold extends StatefulWidget {
/// ), /// ),
/// home: Scaffold( /// home: Scaffold(
/// body: MyScaffoldBody(), /// body: MyScaffoldBody(),
/// appBar: AppBar(title: Text('Scaffold.of Example')), /// appBar: AppBar(title: const Text('Scaffold.of Example')),
/// ), /// ),
/// color: Colors.white, /// color: Colors.white,
/// ); /// );
...@@ -1401,14 +1765,32 @@ class Scaffold extends StatefulWidget { ...@@ -1401,14 +1765,32 @@ class Scaffold extends StatefulWidget {
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return Center( /// return Center(
/// child: ElevatedButton( /// child: ElevatedButton(
/// child: Text('SHOW A SNACKBAR'), /// child: const Text('SHOW BOTTOM SHEET'),
/// onPressed: () { /// onPressed: () {
/// Scaffold.of(context).showSnackBar( /// Scaffold.of(context).showBottomSheet<void>(
/// SnackBar( /// (BuildContext context) {
/// content: Text('Have a snack!'), /// 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);
/// },
/// )
/// ],
/// ),
/// ), /// ),
/// ); /// );
/// }, /// },
/// );
/// },
/// ), /// ),
/// ); /// );
/// } /// }
...@@ -1427,20 +1809,38 @@ class Scaffold extends StatefulWidget { ...@@ -1427,20 +1809,38 @@ class Scaffold extends StatefulWidget {
/// ```dart /// ```dart
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return Scaffold( /// return Scaffold(
/// appBar: AppBar( /// appBar: AppBar(title: const Text('Demo')),
/// title: Text('Demo')
/// ),
/// body: Builder( /// body: Builder(
/// // Create an inner BuildContext so that the onPressed methods /// // Create an inner BuildContext so that the onPressed methods
/// // can refer to the Scaffold with Scaffold.of(). /// // can refer to the Scaffold with Scaffold.of().
/// builder: (BuildContext context) { /// builder: (BuildContext context) {
/// return Center( /// return Center(
/// child: ElevatedButton( /// child: ElevatedButton(
/// child: Text('SHOW A SNACKBAR'), /// child: const Text('SHOW BOTTOM SHEET'),
/// onPressed: () { /// onPressed: () {
/// Scaffold.of(context).showSnackBar(SnackBar( /// Scaffold.of(context).showBottomSheet<void>(
/// content: Text('Have a snack!'), /// (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);
/// },
/// )
/// ],
/// ),
/// ),
/// );
/// },
/// );
/// }, /// },
/// ), /// ),
/// ); /// );
...@@ -1557,7 +1957,7 @@ class Scaffold extends StatefulWidget { ...@@ -1557,7 +1957,7 @@ class Scaffold extends StatefulWidget {
/// See also: /// See also:
/// ///
/// * [Scaffold.of], which provides access to the [ScaffoldState] object as a /// * [Scaffold.of], which provides access to the [ScaffoldState] object as a
/// whole, from which you can show snackbars, bottom sheets, and so forth. /// whole, from which you can show bottom sheets, and so forth.
static bool hasDrawer(BuildContext context, { bool registerForUpdates = true }) { static bool hasDrawer(BuildContext context, { bool registerForUpdates = true }) {
assert(registerForUpdates != null); assert(registerForUpdates != null);
assert(context != null); assert(context != null);
...@@ -1576,8 +1976,8 @@ class Scaffold extends StatefulWidget { ...@@ -1576,8 +1976,8 @@ class Scaffold extends StatefulWidget {
/// State for a [Scaffold]. /// State for a [Scaffold].
/// ///
/// Can display [SnackBar]s and [BottomSheet]s. Retrieve a [ScaffoldState] from /// Can display [BottomSheet]s. Retrieve a [ScaffoldState] from the current
/// the current [BuildContext] using [Scaffold.of]. /// [BuildContext] using [Scaffold.of].
class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
// DRAWER API // DRAWER API
...@@ -1668,12 +2068,10 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1668,12 +2068,10 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
// SNACKBAR API // SNACKBAR API
final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>(); ScaffoldMessengerState _scaffoldMessenger;
AnimationController _snackBarController;
Timer _snackBarTimer;
bool _accessibleNavigation;
/// Shows a [SnackBar] at the bottom of the scaffold. /// [ScaffoldMessengerState.showSnackBar] shows a [SnackBar] at the bottom of
/// the scaffold. This method should not be used.
/// ///
/// A scaffold can show at most one snack bar at a time. If this function is /// A scaffold can show at most one snack bar at a time. If this function is
/// called while another snack bar is already visible, the given snack bar /// called while another snack bar is already visible, the given snack bar
...@@ -1682,12 +2080,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1682,12 +2080,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// ///
/// To control how long a [SnackBar] remains visible, use [SnackBar.duration]. /// To control how long a [SnackBar] remains visible, use [SnackBar.duration].
/// ///
/// To remove the [SnackBar] with an exit animation, use [hideCurrentSnackBar] /// To remove the [SnackBar] with an exit animation, use
/// or call [ScaffoldFeatureController.close] on the returned /// [ScaffoldMessengerState.hideCurrentSnackBar] or call
/// [ScaffoldFeatureController]. To remove a [SnackBar] suddenly (without an /// [ScaffoldFeatureController.close] on the returned [ScaffoldFeatureController].
/// animation), use [removeCurrentSnackBar]. /// To remove a [SnackBar] suddenly (without an animation), use
/// [ScaffoldMessengerState.removeCurrentSnackBar].
/// ///
/// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. /// See [ScaffoldMessenger.of] for information about how to obtain the
/// [ScaffoldMessengerState].
/// ///
/// {@tool dartpad --template=stateless_widget_scaffold_center} /// {@tool dartpad --template=stateless_widget_scaffold_center}
/// ///
...@@ -1697,104 +2097,66 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1697,104 +2097,66 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return OutlinedButton( /// return OutlinedButton(
/// onPressed: () { /// onPressed: () {
/// Scaffold.of(context).showSnackBar( /// ScaffoldMessenger.of(context).showSnackBar(
/// SnackBar( /// SnackBar(
/// content: Text('A SnackBar has been shown.'), /// content: const Text('A SnackBar has been shown.'),
/// ), /// ),
/// ); /// );
/// }, /// },
/// child: Text('Show SnackBar'), /// child: const Text('Show SnackBar'),
/// ); /// );
/// } /// }
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
///
/// See also:
///
/// * [ScaffoldMessenger], this should be used instead to manage [SnackBar]s.
// TODO(Piinks): Deprecate after customers are migrated
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackbar) { ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackbar) {
_snackBarController ??= SnackBar.createAnimationController(vsync: this) assert(debugCheckHasScaffoldMessenger(context));
..addStatusListener(_handleSnackBarStatusChange); return _scaffoldMessenger.showSnackBar(snackbar);
if (_snackBars.isEmpty) {
assert(_snackBarController.isDismissed);
_snackBarController.forward();
}
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> controller;
controller = ScaffoldFeatureController<SnackBar, SnackBarClosedReason>._(
// We provide a fallback key so that if back-to-back snackbars happen to
// match in structure, material ink splashes and highlights don't survive
// from one to the next.
snackbar.withAnimation(_snackBarController, fallbackKey: UniqueKey()),
Completer<SnackBarClosedReason>(),
() {
assert(_snackBars.first == controller);
hideCurrentSnackBar(reason: SnackBarClosedReason.hide);
},
null, // SnackBar doesn't use a builder function so setState() wouldn't rebuild it
);
setState(() {
_snackBars.addLast(controller);
});
return controller;
} }
void _handleSnackBarStatusChange(AnimationStatus status) { /// [ScaffoldMessengerState.removeCurrentSnackBar] removes the current
switch (status) { /// [SnackBar] (if any) immediately. This method should not be used.
case AnimationStatus.dismissed:
assert(_snackBars.isNotEmpty);
setState(() {
_snackBars.removeFirst();
});
if (_snackBars.isNotEmpty)
_snackBarController.forward();
break;
case AnimationStatus.completed:
setState(() {
assert(_snackBarTimer == null);
// build will create a new timer if necessary to dismiss the snack bar
});
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
}
}
/// Removes the current [SnackBar] (if any) immediately.
/// ///
/// The removed snack bar does not run its normal exit animation. If there are /// The removed snack bar does not run its normal exit animation. If there are
/// any queued snack bars, they begin their entrance animation immediately. /// any queued snack bars, they begin their entrance animation immediately.
///
/// See also:
///
/// * [ScaffoldMessenger], this should be used instead to manage [SnackBar]s.
// TODO(Piinks): Deprecate after customers are migrated
void removeCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.remove }) { void removeCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.remove }) {
assert(reason != null); assert(debugCheckHasScaffoldMessenger(context));
if (_snackBars.isEmpty) _scaffoldMessenger.removeCurrentSnackBar(reason: reason);
return;
final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
if (!completer.isCompleted)
completer.complete(reason);
_snackBarTimer?.cancel();
_snackBarTimer = null;
_snackBarController.value = 0.0;
} }
/// Removes the current [SnackBar] by running its normal exit animation. /// [ScaffoldMessengerState.hideCurrentSnackBar] removes the current
/// [SnackBar] by running its normal exit animation. This method should not be
/// used.
/// ///
/// The closed completer is called after the animation is complete. /// The closed completer is called after the animation is complete.
///
/// See also:
///
/// * [ScaffoldMessenger], this should be used instead to manage [SnackBar]s.
// TODO(Piinks): Deprecate after customers are migrated.
void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) { void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) {
assert(reason != null); assert(debugCheckHasScaffoldMessenger(context));
if (_snackBars.isEmpty || _snackBarController.status == AnimationStatus.dismissed) _scaffoldMessenger.hideCurrentSnackBar(reason: reason);
return;
final MediaQueryData mediaQuery = MediaQuery.of(context);
final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
if (mediaQuery.accessibleNavigation) {
_snackBarController.value = 0.0;
completer.complete(reason);
} else {
_snackBarController.reverse().then<void>((void value) {
assert(mounted);
if (!completer.isCompleted)
completer.complete(reason);
});
}
_snackBarTimer?.cancel();
_snackBarTimer = null;
} }
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> _snackBar;
void _updateSnackBar() {
setState(() {
_snackBar = _scaffoldMessenger._snackBars.isNotEmpty
? _scaffoldMessenger._snackBars.first
: null;
});
}
// PERSISTENT BOTTOM SHEET API // PERSISTENT BOTTOM SHEET API
...@@ -2016,7 +2378,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2016,7 +2378,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// const Text('BottomSheet'), /// const Text('BottomSheet'),
/// ElevatedButton( /// ElevatedButton(
/// child: const Text('Close BottomSheet'), /// child: const Text('Close BottomSheet'),
/// onPressed: () => Navigator.pop(context), /// onPressed: () {
/// Navigator.pop(context);
/// }
/// ) /// )
/// ], /// ],
/// ), /// ),
...@@ -2204,27 +2568,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2204,27 +2568,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
final MediaQueryData mediaQuery = MediaQuery.of(context); _scaffoldMessenger = ScaffoldMessenger.of(context);
// If we transition from accessible navigation to non-accessible navigation _scaffoldMessenger?._register(this);
// and there is a SnackBar that would have timed out that has already
// completed its timer, dismiss that SnackBar. If the timer hasn't finished
// yet, let it timeout as normal.
if (_accessibleNavigation == true
&& !mediaQuery.accessibleNavigation
&& _snackBarTimer != null
&& !_snackBarTimer.isActive) {
hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
}
_accessibleNavigation = mediaQuery.accessibleNavigation;
_maybeBuildPersistentBottomSheet(); _maybeBuildPersistentBottomSheet();
super.didChangeDependencies(); super.didChangeDependencies();
} }
@override @override
void dispose() { void dispose() {
_snackBarController?.dispose();
_snackBarTimer?.cancel();
_snackBarTimer = null;
_geometryNotifier.dispose(); _geometryNotifier.dispose();
for (final _StandardBottomSheet bottomSheet in _dismissedBottomSheets) { for (final _StandardBottomSheet bottomSheet in _dismissedBottomSheets) {
bottomSheet.animationController?.dispose(); bottomSheet.animationController?.dispose();
...@@ -2234,6 +2585,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2234,6 +2585,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
} }
_floatingActionButtonMoveController.dispose(); _floatingActionButtonMoveController.dispose();
_floatingActionButtonVisibilityController.dispose(); _floatingActionButtonVisibilityController.dispose();
_scaffoldMessenger?._unregister(this);
super.dispose(); super.dispose();
} }
...@@ -2347,28 +2699,6 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2347,28 +2699,6 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
final MediaQueryData mediaQuery = MediaQuery.of(context); final MediaQueryData mediaQuery = MediaQuery.of(context);
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
final TextDirection textDirection = Directionality.of(context); final TextDirection textDirection = Directionality.of(context);
_accessibleNavigation = mediaQuery.accessibleNavigation;
if (_snackBars.isNotEmpty) {
final ModalRoute<dynamic> route = ModalRoute.of(context);
if (route == null || route.isCurrent) {
if (_snackBarController.isCompleted && _snackBarTimer == null) {
final SnackBar snackBar = _snackBars.first._widget;
_snackBarTimer = Timer(snackBar.duration, () {
assert(_snackBarController.status == AnimationStatus.forward ||
_snackBarController.status == AnimationStatus.completed);
// Look up MediaQuery again in case the setting changed.
final MediaQueryData mediaQuery = MediaQuery.of(context);
if (mediaQuery.accessibleNavigation && snackBar.action != null)
return;
hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
});
}
} else {
_snackBarTimer?.cancel();
_snackBarTimer = null;
}
}
final List<LayoutId> children = <LayoutId>[]; final List<LayoutId> children = <LayoutId>[];
_addIfNonNull( _addIfNonNull(
...@@ -2423,16 +2753,16 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2423,16 +2753,16 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
bool isSnackBarFloating = false; bool isSnackBarFloating = false;
double snackBarWidth; double snackBarWidth;
if (_snackBars.isNotEmpty) { if (_snackBar != null) {
final SnackBarBehavior snackBarBehavior = _snackBars.first._widget.behavior final SnackBarBehavior snackBarBehavior = _snackBar._widget.behavior
?? themeData.snackBarTheme.behavior ?? themeData.snackBarTheme.behavior
?? SnackBarBehavior.fixed; ?? SnackBarBehavior.fixed;
isSnackBarFloating = snackBarBehavior == SnackBarBehavior.floating; isSnackBarFloating = snackBarBehavior == SnackBarBehavior.floating;
snackBarWidth = _snackBars.first._widget.width; snackBarWidth = _snackBar._widget.width;
_addIfNonNull( _addIfNonNull(
children, children,
_snackBars.first._widget, _snackBar._widget,
_ScaffoldSlot.snackBar, _ScaffoldSlot.snackBar,
removeLeftPadding: false, removeLeftPadding: false,
removeTopPadding: true, removeTopPadding: true,
...@@ -2596,7 +2926,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2596,7 +2926,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// An interface for controlling a feature of a [Scaffold]. /// An interface for controlling a feature of a [Scaffold].
/// ///
/// Commonly obtained from [ScaffoldState.showSnackBar] or [ScaffoldState.showBottomSheet]. /// Commonly obtained from [ScaffoldMessengerState.showSnackBar] or
/// [ScaffoldState.showBottomSheet].
class ScaffoldFeatureController<T extends Widget, U> { class ScaffoldFeatureController<T extends Widget, U> {
const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState); const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState);
final T _widget; final T _widget;
......
...@@ -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!')),
); );
}, },
......
...@@ -18,7 +18,7 @@ void main() { ...@@ -18,7 +18,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(const SnackBar( ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(helloSnackBar), content: Text(helloSnackBar),
duration: Duration(seconds: 2), duration: Duration(seconds: 2),
)); ));
...@@ -64,7 +64,7 @@ void main() { ...@@ -64,7 +64,7 @@ void main() {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
snackBarCount += 1; snackBarCount += 1;
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('bar$snackBarCount'), content: Text('bar$snackBarCount'),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
)); ));
...@@ -141,7 +141,7 @@ void main() { ...@@ -141,7 +141,7 @@ void main() {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
snackBarCount += 1; snackBarCount += 1;
lastController = Scaffold.of(context).showSnackBar(SnackBar( lastController = ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('bar$snackBarCount'), content: Text('bar$snackBarCount'),
duration: Duration(seconds: time), duration: Duration(seconds: time),
)); ));
...@@ -225,7 +225,7 @@ void main() { ...@@ -225,7 +225,7 @@ void main() {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
snackBarCount += 1; snackBarCount += 1;
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('bar$snackBarCount'), content: Text('bar$snackBarCount'),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
)); ));
...@@ -268,7 +268,7 @@ void main() { ...@@ -268,7 +268,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( action: SnackBarAction(
...@@ -309,7 +309,7 @@ void main() { ...@@ -309,7 +309,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( 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),
...@@ -351,7 +351,7 @@ void main() { ...@@ -351,7 +351,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( 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),
...@@ -389,7 +389,7 @@ void main() { ...@@ -389,7 +389,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text('I am a snack bar.'), content: const Text('I am a snack bar.'),
margin: const EdgeInsets.all(padding), margin: const EdgeInsets.all(padding),
...@@ -476,7 +476,7 @@ void main() { ...@@ -476,7 +476,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('I am a snack bar.'), content: Text('I am a snack bar.'),
padding: EdgeInsets.all(padding), padding: EdgeInsets.all(padding),
...@@ -520,7 +520,7 @@ void main() { ...@@ -520,7 +520,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text('I am a snack bar.'), content: const Text('I am a snack bar.'),
width: width, width: width,
...@@ -558,7 +558,7 @@ void main() { ...@@ -558,7 +558,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( 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),
...@@ -611,7 +611,7 @@ void main() { ...@@ -611,7 +611,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: () { }),
...@@ -666,7 +666,7 @@ void main() { ...@@ -666,7 +666,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: () {}),
...@@ -717,7 +717,7 @@ void main() { ...@@ -717,7 +717,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: () {}),
...@@ -771,7 +771,7 @@ void main() { ...@@ -771,7 +771,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: () {}),
...@@ -829,7 +829,7 @@ void main() { ...@@ -829,7 +829,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: () {}),
...@@ -861,18 +861,18 @@ void main() { ...@@ -861,18 +861,18 @@ void main() {
}); });
testWidgets('SnackBarClosedReason', (WidgetTester tester) async { testWidgets('SnackBarClosedReason', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
bool actionPressed = false; bool actionPressed = false;
SnackBarClosedReason closedReason; SnackBarClosedReason closedReason;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
scaffoldMessengerKey: scaffoldMessengerKey,
home: Scaffold( home: Scaffold(
key: scaffoldKey,
body: Builder( body: Builder(
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('snack'), content: const Text('snack'),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
action: SnackBarAction( action: SnackBarAction(
...@@ -917,14 +917,14 @@ void main() { ...@@ -917,14 +917,14 @@ void main() {
// Pop up the snack bar and then remove it. // Pop up the snack bar and then remove it.
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(const Duration(milliseconds: 750)); await tester.pump(const Duration(milliseconds: 750));
scaffoldKey.currentState.removeCurrentSnackBar(); scaffoldMessengerKey.currentState.removeCurrentSnackBar();
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
expect(closedReason, equals(SnackBarClosedReason.remove)); expect(closedReason, equals(SnackBarClosedReason.remove));
// Pop up the snack bar and then hide it. // Pop up the snack bar and then hide it.
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(const Duration(milliseconds: 750)); await tester.pump(const Duration(milliseconds: 750));
scaffoldKey.currentState.hideCurrentSnackBar(); scaffoldMessengerKey.currentState.hideCurrentSnackBar();
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
expect(closedReason, equals(SnackBarClosedReason.hide)); expect(closedReason, equals(SnackBarClosedReason.hide));
...@@ -944,13 +944,14 @@ void main() { ...@@ -944,13 +944,14 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: MediaQuery( home: MediaQuery(
data: const MediaQueryData(accessibleNavigation: true), data: const MediaQueryData(accessibleNavigation: true),
child: Scaffold( child: ScaffoldMessenger(
key: scaffoldKey, child: Builder(
body: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return Scaffold(
key: scaffoldKey,
body: GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('snack'), content: const Text('snack'),
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
action: SnackBarAction( action: SnackBarAction(
...@@ -960,9 +961,10 @@ void main() { ...@@ -960,9 +961,10 @@ void main() {
)); ));
}, },
child: const Text('X'), child: const Text('X'),
);
},
), ),
);
}
)
), ),
), ),
)); ));
...@@ -986,13 +988,13 @@ void main() { ...@@ -986,13 +988,13 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: MediaQuery( home: MediaQuery(
data: const MediaQueryData(accessibleNavigation: true), data: const MediaQueryData(accessibleNavigation: true),
child: Scaffold( child: ScaffoldMessenger(
child: Builder(builder: (BuildContext context) {
return Scaffold(
key: scaffoldKey, key: scaffoldKey,
body: Builder( body: GestureDetector(
builder: (BuildContext context) {
return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('snack'), content: const Text('snack'),
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
action: SnackBarAction( action: SnackBarAction(
...@@ -1002,11 +1004,11 @@ void main() { ...@@ -1002,11 +1004,11 @@ void main() {
)); ));
}, },
child: const Text('X'), child: const Text('X'),
);
},
),
), ),
);
}),
), ),
)
)); ));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -1031,7 +1033,7 @@ void main() { ...@@ -1031,7 +1033,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(const SnackBar( ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(helloSnackBar), content: Text(helloSnackBar),
)); ));
}, },
...@@ -1080,7 +1082,7 @@ void main() { ...@@ -1080,7 +1082,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('test'), content: const Text('test'),
action: SnackBarAction(label: 'foo', onPressed: () { }), action: SnackBarAction(label: 'foo', onPressed: () { }),
)); ));
...@@ -1126,7 +1128,7 @@ void main() { ...@@ -1126,7 +1128,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: Text(nonconst('hello')), content: Text(nonconst('hello')),
duration: null, duration: null,
)); ));
...@@ -1156,7 +1158,7 @@ void main() { ...@@ -1156,7 +1158,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('hello'), content: const Text('hello'),
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
onVisible: () { onVisible: () {
...@@ -1193,14 +1195,14 @@ void main() { ...@@ -1193,14 +1195,14 @@ 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('hello'), content: const Text('hello'),
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
onVisible: () { onVisible: () {
called += 1; called += 1;
}, },
)); ));
Scaffold.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('hello 2'), content: const Text('hello 2'),
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
onVisible: () { onVisible: () {
...@@ -1247,8 +1249,8 @@ void main() { ...@@ -1247,8 +1249,8 @@ void main() {
), ),
); );
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold)); final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldState.showSnackBar(snackBar); scaffoldMessengerState.showSnackBar(snackBar);
await tester.pumpAndSettle(); // Have the SnackBar fully animate out. await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
...@@ -1278,8 +1280,8 @@ void main() { ...@@ -1278,8 +1280,8 @@ void main() {
), ),
); );
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold)); final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldState.showSnackBar(snackBar); scaffoldMessengerState.showSnackBar(snackBar);
await tester.pumpAndSettle(); // Have the SnackBar fully animate out. await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
...@@ -1298,9 +1300,8 @@ void main() { ...@@ -1298,9 +1300,8 @@ void main() {
testWidgets( testWidgets(
'Padding of $behavior is not consumed by viewInsets', 'Padding of $behavior is not consumed by viewInsets',
(WidgetTester tester) async { (WidgetTester tester) async {
final Widget child = Directionality( final Widget child = MaterialApp(
textDirection: TextDirection.ltr, home: Scaffold(
child: Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.send), child: const Icon(Icons.send),
...@@ -1310,7 +1311,7 @@ void main() { ...@@ -1310,7 +1311,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( 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),
...@@ -1374,8 +1375,8 @@ void main() { ...@@ -1374,8 +1375,8 @@ void main() {
), ),
); );
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold)); final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldState.showSnackBar( scaffoldMessengerState.showSnackBar(
const SnackBar( const SnackBar(
content: Text('Snackbar text'), content: Text('Snackbar text'),
behavior: SnackBarBehavior.fixed, behavior: SnackBarBehavior.fixed,
...@@ -1410,7 +1411,7 @@ void main() { ...@@ -1410,7 +1411,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: () {}),
...@@ -1452,8 +1453,8 @@ void main() { ...@@ -1452,8 +1453,8 @@ void main() {
), ),
); );
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold)); final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldState.showSnackBar( scaffoldMessengerState.showSnackBar(
const SnackBar( const SnackBar(
content: Text('SnackBar text'), content: Text('SnackBar text'),
behavior: SnackBarBehavior.fixed, behavior: SnackBarBehavior.fixed,
...@@ -1489,8 +1490,8 @@ void main() { ...@@ -1489,8 +1490,8 @@ void main() {
), ),
); );
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold)); final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldState.showSnackBar( scaffoldMessengerState.showSnackBar(
const SnackBar( const SnackBar(
content: Text('SnackBar text'), content: Text('SnackBar text'),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
...@@ -1506,4 +1507,74 @@ void main() { ...@@ -1506,4 +1507,74 @@ void main() {
}, },
); );
}); });
testWidgets('SnackBars hero across transitions', (WidgetTester tester) async {
const String snackBarText = 'hello snackbar';
const String firstHeader = 'home';
const String secondHeader = 'second';
const Key snackTarget = Key('snack-target');
const Key transitionTarget = Key('transition-target');
Widget _buildApp() {
return MaterialApp(
routes: <String, WidgetBuilder> {
'/': (BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text(firstHeader)),
body: Center(
child: ElevatedButton(
key: transitionTarget,
child: const Text('PUSH'),
onPressed: () {
Navigator.of(context).pushNamed('/second');
},
),
),
floatingActionButton: FloatingActionButton(
key: snackTarget,
onPressed: () async {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(snackBarText),
),
);
},
child: const Text('X'),
),
);
},
'/second': (BuildContext context) => Scaffold(appBar: AppBar(title: const Text(secondHeader)),
),
}
);
}
await tester.pumpWidget(_buildApp());
expect(find.text(snackBarText), findsNothing);
expect(find.text(firstHeader), findsOneWidget);
expect(find.text(secondHeader), findsNothing);
// Present SnackBar
await tester.tap(find.byKey(snackTarget));
await tester.pump(); // schedule animation
expect(find.text(snackBarText), findsOneWidget);
await tester.pump(); // begin animation
expect(find.text(snackBarText), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750));
expect(find.text(snackBarText), findsOneWidget);
// Push new route
await tester.tap(find.byKey(transitionTarget));
await tester.pump();
expect(find.text(snackBarText), findsOneWidget);
expect(find.text(firstHeader), findsOneWidget);
expect(find.text(secondHeader, skipOffstage: false), findsOneWidget);
await tester.pump();
expect(find.text(snackBarText), findsOneWidget);
expect(find.text(firstHeader), findsOneWidget);
expect(find.text(secondHeader), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750));
expect(find.text(snackBarText), findsOneWidget);
expect(find.text(firstHeader), findsNothing);
expect(find.text(secondHeader), findsOneWidget);
});
} }
...@@ -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