Unverified Commit 64c845c5 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Re-land ScaffoldMessenger (#66504)

parent 7f2ca5e5
...@@ -15,6 +15,7 @@ import 'floating_action_button.dart'; ...@@ -15,6 +15,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' show ScaffoldMessenger, ScaffoldMessengerState;
import 'theme.dart'; import 'theme.dart';
/// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage /// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage
...@@ -166,6 +167,7 @@ class MaterialApp extends StatefulWidget { ...@@ -166,6 +167,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,
...@@ -214,6 +216,7 @@ class MaterialApp extends StatefulWidget { ...@@ -214,6 +216,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;
...@@ -724,7 +735,9 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -724,7 +735,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
...@@ -745,6 +758,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -745,6 +758,7 @@ class _MaterialAppState extends State<MaterialApp> {
}, },
) )
: child!, : child!,
)
); );
} }
......
...@@ -135,7 +135,7 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate { ...@@ -135,7 +135,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(
......
...@@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart'; ...@@ -7,7 +7,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.
/// ///
...@@ -123,3 +123,34 @@ bool debugCheckHasScaffold(BuildContext context) { ...@@ -123,3 +123,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.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;
}
...@@ -17,6 +17,7 @@ import 'bottom_sheet.dart'; ...@@ -17,6 +17,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';
...@@ -59,6 +60,402 @@ enum _ScaffoldSlot { ...@@ -59,6 +60,402 @@ 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.
///
/// {@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}
///
/// See also:
///
/// * [SnackBar], which is a temporary notification typically shown near the
/// bottom of the app using the [ScaffoldMessengerState.showSnackBar] method.
/// * [debugCheckHasScaffoldMessenger], which asserts that the given context
/// has a [ScaffoldMessenger] ancestor.
/// * 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 to 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 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 return null.
/// See also:
///
/// * [debugCheckHasScaffoldMessenger], which asserts that the given context
/// has a [ScaffoldMessenger] ancestor.
static ScaffoldMessengerState? of(BuildContext context, { bool nullOk = false }) {
assert(nullOk != null);
assert(context != null);
assert(nullOk || debugCheckHasScaffoldMessenger(context));
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();
}
late 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,
required Widget child,
required 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].
/// ///
...@@ -835,11 +1232,11 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -835,11 +1232,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].
...@@ -916,7 +1313,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -916,7 +1313,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.'),
...@@ -1008,8 +1405,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -1008,8 +1405,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
...@@ -1017,7 +1412,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -1017,7 +1412,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 {
...@@ -1389,7 +1783,7 @@ class Scaffold extends StatefulWidget { ...@@ -1389,7 +1783,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,
/// ); /// );
...@@ -1403,14 +1797,32 @@ class Scaffold extends StatefulWidget { ...@@ -1403,14 +1797,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);
/// },
/// )
/// ],
/// ),
/// ), /// ),
/// ); /// );
/// }, /// },
/// );
/// },
/// ), /// ),
/// ); /// );
/// } /// }
...@@ -1429,20 +1841,38 @@ class Scaffold extends StatefulWidget { ...@@ -1429,20 +1841,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);
/// },
/// )
/// ],
/// ),
/// ),
/// );
/// },
/// );
/// }, /// },
/// ), /// ),
/// ); /// );
...@@ -1559,7 +1989,7 @@ class Scaffold extends StatefulWidget { ...@@ -1559,7 +1989,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);
...@@ -1578,8 +2008,8 @@ class Scaffold extends StatefulWidget { ...@@ -1578,8 +2008,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
...@@ -1669,13 +2099,15 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1669,13 +2099,15 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
} }
// SNACKBAR API // SNACKBAR API
final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>(); final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
AnimationController? _snackBarController; AnimationController? _snackBarController;
Timer? _snackBarTimer; Timer? _snackBarTimer;
bool? _accessibleNavigation; bool? _accessibleNavigation;
ScaffoldMessengerState? _scaffoldMessenger;
/// 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, and will be deprecated in
/// the near future..
/// ///
/// 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
...@@ -1684,12 +2116,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1684,12 +2116,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}
/// ///
...@@ -1699,17 +2133,22 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1699,17 +2133,22 @@ 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 & defer to ScaffoldMessenger after customers are migrated.
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackbar) { ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackbar) {
_snackBarController ??= SnackBar.createAnimationController(vsync: this) _snackBarController ??= SnackBar.createAnimationController(vsync: this)
..addStatusListener(_handleSnackBarStatusChange); ..addStatusListener(_handleSnackBarStatusChange);
...@@ -1758,12 +2197,39 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1758,12 +2197,39 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
} }
} }
/// Removes the current [SnackBar] (if any) immediately. /// [ScaffoldMessengerState.removeCurrentSnackBar] removes the current
/// [SnackBar] (if any) immediately. This method should not be used, and will
/// be depracted in the near future.
/// ///
/// 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 & defer to ScaffoldMessenger after customers are migrated
void removeCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.remove }) { void removeCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.remove }) {
assert(reason != null); assert(reason != null);
// SnackBars and SnackBarActions can call to hide and remove themselves, but
// they are not aware of who presented them, the Scaffold or the
// ScaffoldMessenger. As such, when the SnackBar classes call upon Scaffold
// to remove (the current default), we should re-direct to the
// ScaffoldMessenger here if that is where the SnackBar originated from.
if (_messengerSnackBar != null) {
// ScaffoldMessenger is presenting SnackBars.
assert(debugCheckHasScaffoldMessenger(context));
assert(
_scaffoldMessenger != null,
'A SnackBar was shown by the ScaffoldMessenger, but has been called upon'
'to be removed from a Scaffold that is not registered with a '
'ScaffoldMessenger, this can happen if a Scaffold has been rebuilt '
'without an ancestor ScaffoldMessenger.',
);
_scaffoldMessenger!.removeCurrentSnackBar(reason: reason);
return;
}
if (_snackBars.isEmpty) if (_snackBars.isEmpty)
return; return;
final Completer<SnackBarClosedReason> completer = _snackBars.first._completer; final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
...@@ -1774,11 +2240,38 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1774,11 +2240,38 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_snackBarController!.value = 0.0; _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, and will be deprecated in the near future.
/// ///
/// 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 & defer to ScaffoldMessenger after customers are migrated.
void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) { void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) {
assert(reason != null); assert(reason != null);
// SnackBars and SnackBarActions can call to hide and remove themselves, but
// they are not aware of who presented them, the Scaffold or the
// ScaffoldMessenger. As such, when the SnackBar classes call upon Scaffold
// to remove (the current default), we should re-direct to the
// ScaffoldMessenger here if that is where the SnackBar originated from.
if (_messengerSnackBar != null) {
// ScaffoldMessenger is presenting SnackBars.
assert(debugCheckHasScaffoldMessenger(context));
assert(
_scaffoldMessenger != null,
'A SnackBar was shown by the ScaffoldMessenger, but has been called upon'
'to be removed from a Scaffold that is not registered with a '
'ScaffoldMessenger, this can happen if a Scaffold has been rebuilt '
'without an ancestor ScaffoldMessenger.',
);
_scaffoldMessenger!.hideCurrentSnackBar(reason: reason);
return;
}
if (_snackBars.isEmpty || _snackBarController!.status == AnimationStatus.dismissed) if (_snackBars.isEmpty || _snackBarController!.status == AnimationStatus.dismissed)
return; return;
final MediaQueryData mediaQuery = MediaQuery.of(context)!; final MediaQueryData mediaQuery = MediaQuery.of(context)!;
...@@ -1797,6 +2290,18 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1797,6 +2290,18 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_snackBarTimer = null; _snackBarTimer = null;
} }
// The _messengerSnackBar represents the current SnackBar being managed by
// the ScaffoldMessenger, instead of the Scaffold.
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? _messengerSnackBar;
// This is used to update the _messengerSnackBar by the ScaffoldMessenger.
void _updateSnackBar() {
setState(() {
_messengerSnackBar = _scaffoldMessenger!._snackBars.isNotEmpty
? _scaffoldMessenger!._snackBars.first
: null;
});
}
// PERSISTENT BOTTOM SHEET API // PERSISTENT BOTTOM SHEET API
...@@ -2010,7 +2515,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2010,7 +2515,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);
/// }
/// ) /// )
/// ], /// ],
/// ), /// ),
...@@ -2198,6 +2705,19 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2198,6 +2705,19 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
// nullOk is valid here since both the Scaffold and ScaffoldMessenger are
// currently available for managing SnackBars.
final ScaffoldMessengerState? _currentScaffoldMessenger = ScaffoldMessenger.of(context, nullOk: true);
// If our ScaffoldMessenger has changed, unregister with the old one first.
if (_scaffoldMessenger != null &&
(_currentScaffoldMessenger == null || _scaffoldMessenger != _currentScaffoldMessenger)) {
_scaffoldMessenger?._unregister(this);
}
// Register with the current ScaffoldMessenger, if there is one.
_scaffoldMessenger = _currentScaffoldMessenger;
_scaffoldMessenger?._register(this);
// TODO(Piinks): Remove old SnackBar API after migrating ScaffoldMessenger
final MediaQueryData mediaQuery = MediaQuery.of(context)!; final MediaQueryData mediaQuery = MediaQuery.of(context)!;
// If we transition from accessible navigation to non-accessible navigation // If we transition from accessible navigation to non-accessible navigation
// and there is a SnackBar that would have timed out that has already // and there is a SnackBar that would have timed out that has already
...@@ -2210,15 +2730,18 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2210,15 +2730,18 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
} }
_accessibleNavigation = mediaQuery.accessibleNavigation; _accessibleNavigation = mediaQuery.accessibleNavigation;
_maybeBuildPersistentBottomSheet(); _maybeBuildPersistentBottomSheet();
super.didChangeDependencies(); super.didChangeDependencies();
} }
@override @override
void dispose() { void dispose() {
// TODO(Piinks): Remove old SnackBar API after migrating ScaffoldMessenger
_snackBarController?.dispose(); _snackBarController?.dispose();
_snackBarTimer?.cancel(); _snackBarTimer?.cancel();
_snackBarTimer = null; _snackBarTimer = null;
_geometryNotifier.dispose(); _geometryNotifier.dispose();
for (final _StandardBottomSheet bottomSheet in _dismissedBottomSheets) { for (final _StandardBottomSheet bottomSheet in _dismissedBottomSheets) {
bottomSheet.animationController.dispose(); bottomSheet.animationController.dispose();
...@@ -2228,6 +2751,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2228,6 +2751,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
} }
_floatingActionButtonMoveController.dispose(); _floatingActionButtonMoveController.dispose();
_floatingActionButtonVisibilityController.dispose(); _floatingActionButtonVisibilityController.dispose();
_scaffoldMessenger?._unregister(this);
super.dispose(); super.dispose();
} }
...@@ -2341,8 +2865,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2341,8 +2865,9 @@ 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;
// TODO(Piinks): Remove old SnackBar API after migrating ScaffoldMessenger
_accessibleNavigation = mediaQuery.accessibleNavigation;
if (_snackBars.isNotEmpty) { if (_snackBars.isNotEmpty) {
final ModalRoute<dynamic>? route = ModalRoute.of(context); final ModalRoute<dynamic>? route = ModalRoute.of(context);
if (route == null || route.isCurrent) { if (route == null || route.isCurrent) {
...@@ -2417,6 +2942,38 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2417,6 +2942,38 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
bool isSnackBarFloating = false; bool isSnackBarFloating = false;
double? snackBarWidth; double? snackBarWidth;
// We should only be using one API for SnackBars. Currently, we can use the
// Scaffold, which creates a SnackBar queue (_snackBars), or the
// ScaffoldMessenger, which sends a SnackBar to descendant Scaffolds.
// (_messengerSnackBar).
assert(
_snackBars.isEmpty || _messengerSnackBar == null,
'Only one API should be used to manage SnackBars. The ScaffoldMessenger is '
'the preferred API instead of the Scaffold methods.'
);
// SnackBar set by ScaffoldMessenger
if (_messengerSnackBar != null) {
final SnackBarBehavior snackBarBehavior = _messengerSnackBar?._widget.behavior
?? themeData.snackBarTheme.behavior
?? SnackBarBehavior.fixed;
isSnackBarFloating = snackBarBehavior == SnackBarBehavior.floating;
snackBarWidth = _messengerSnackBar?._widget.width;
_addIfNonNull(
children,
_messengerSnackBar?._widget,
_ScaffoldSlot.snackBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null,
maintainBottomViewPadding: !_resizeToAvoidBottomInset,
);
}
// SnackBar set by Scaffold
// TODO(Piinks): Remove old SnackBar API after migrating ScaffoldMessenger
if (_snackBars.isNotEmpty) { if (_snackBars.isNotEmpty) {
final SnackBarBehavior snackBarBehavior = _snackBars.first._widget.behavior final SnackBarBehavior snackBarBehavior = _snackBars.first._widget.behavior
?? themeData.snackBarTheme.behavior ?? themeData.snackBarTheme.behavior
...@@ -2590,7 +3147,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2590,7 +3147,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.
...@@ -150,8 +150,8 @@ class _SnackBarActionState extends State<SnackBarAction> { ...@@ -150,8 +150,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].
/// ///
...@@ -160,11 +160,11 @@ class _SnackBarActionState extends State<SnackBarAction> { ...@@ -160,11 +160,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
...@@ -293,7 +293,7 @@ class SnackBar extends StatefulWidget { ...@@ -293,7 +293,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>
...@@ -305,7 +305,7 @@ class SnackBar extends StatefulWidget { ...@@ -305,7 +305,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 }) {
...@@ -342,7 +342,6 @@ class SnackBar extends StatefulWidget { ...@@ -342,7 +342,6 @@ class SnackBar extends StatefulWidget {
State<SnackBar> createState() => _SnackBarState(); State<SnackBar> createState() => _SnackBarState();
} }
class _SnackBarState extends State<SnackBar> { class _SnackBarState extends State<SnackBar> {
bool _wasVisible = false; bool _wasVisible = false;
...@@ -560,6 +559,9 @@ class _SnackBarState extends State<SnackBar> { ...@@ -560,6 +559,9 @@ class _SnackBarState extends State<SnackBar> {
); );
} }
return ClipRect(child: snackBarTransition); return Hero(
child: ClipRect(child: snackBarTransition),
tag: '<SnackBar Hero tag - ${widget.content}>',
);
} }
} }
...@@ -510,6 +510,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -510,6 +510,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.
...@@ -567,7 +568,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -567,7 +568,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),
...@@ -579,6 +580,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi ...@@ -579,6 +580,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(
......
...@@ -2112,7 +2112,7 @@ typedef ElementVisitor = void Function(Element element); ...@@ -2112,7 +2112,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
...@@ -2123,19 +2123,38 @@ typedef ElementVisitor = void Function(Element element); ...@@ -2123,19 +2123,38 @@ typedef ElementVisitor = void Function(Element element);
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// // here, Scaffold.of(context) returns null /// // here, Scaffold.of(context) returns null
/// return Scaffold( /// return Scaffold(
/// appBar: AppBar(title: Text('Demo')), /// appBar: const AppBar(title: Text('Demo')),
/// body: Builder( /// body: Builder(
/// builder: (BuildContext context) { /// builder: (BuildContext context) {
/// return TextButton( /// return TextButton(
/// child: Text('BUTTON'), /// child: const 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),
/// },
/// )
/// ],
/// ),
/// ),
/// ); /// );
/// } /// },
/// );
/// },
/// );
/// },
/// ) /// )
/// ); /// );
/// } /// }
......
...@@ -168,6 +168,8 @@ void main() { ...@@ -168,6 +168,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'
...@@ -204,4 +206,83 @@ void main() { ...@@ -204,4 +206,83 @@ 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>();
final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
final SnackBar snackBar = SnackBar(
content: const Text('Snack'),
action: SnackBarAction(label: 'Test', onPressed: () {})
);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: ScaffoldMessenger(
key: _scaffoldMessengerKey,
child: Builder(
builder: (BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: Container(),
);
}
)
),
)
));
final List<dynamic> exceptions = <dynamic>[];
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
// ScaffoldMessenger shows SnackBar.
_scaffoldMessengerKey.currentState.showSnackBar(snackBar);
await tester.pumpAndSettle();
// Pump widget to rebuild without ScaffoldMessenger
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
key: _scaffoldKey,
body: Container(),
),
),
));
// The Scaffold should assert we still have an ancestor ScaffoldMessenger in
// order to dismiss the SnackBar from the ScaffoldMessenger.
await tester.tap(find.text('Test'));
FlutterError.onError = oldHandler;
expect(exceptions.length, 1);
expect(exceptions.single.runtimeType, FlutterError);
final FlutterError error = exceptions.first as FlutterError;
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>#00829]\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!')),
); );
}, },
......
...@@ -2034,6 +2034,117 @@ void main() { ...@@ -2034,6 +2034,117 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
}); });
testWidgets('ScaffoldMessenger.of can return null', (WidgetTester tester) async {
ScaffoldMessengerState scaffoldMessenger;
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
scaffoldMessenger = ScaffoldMessenger.of(context, nullOk: true);
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 100.0,
width: 100.0,
key: tapTarget,
),
);
}
),
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pump();
expect(scaffoldMessenger, isNull);
});
testWidgets('ScaffoldMessenger.of will assert if !nullOk', (WidgetTester tester) async {
const Key tapTarget = Key('tap-target');
final List<dynamic> exceptions = <dynamic>[];
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
ScaffoldMessenger.of(context);
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 100.0,
width: 100.0,
key: tapTarget,
),
);
}
),
),
),
));
await tester.tap(find.byKey(tapTarget));
FlutterError.onError = oldHandler;
expect(exceptions.length, 1);
expect(exceptions.single.runtimeType, FlutterError);
final FlutterError error = exceptions.first as FlutterError;
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'
' Builder widgets require a ScaffoldMessenger widget ancestor.\n'
' The specific widget that could not find a ScaffoldMessenger\n'
' ancestor was:\n'
' Builder\n'
' The ancestors of this widget were:\n'
' _BodyBuilder\n'
' MediaQuery\n'
' LayoutId-[<_ScaffoldSlot.body>]\n'
' CustomMultiChildLayout\n'
' AnimatedBuilder\n'
' DefaultTextStyle\n'
' AnimatedDefaultTextStyle\n'
' _InkFeatures-[GlobalKey#342d0 ink renderer]\n'
' NotificationListener<LayoutChangedNotification>\n'
' PhysicalModel\n'
' AnimatedPhysicalModel\n'
' Material\n'
' PrimaryScrollController\n'
' _ScaffoldScope\n'
' Scaffold\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'
));
});
} }
class _GeometryListener extends StatefulWidget { class _GeometryListener extends StatefulWidget {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
// @dart = 2.8 // @dart = 2.8
import 'package:flutter/foundation.dart' show FlutterExceptionHandler;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -54,6 +55,51 @@ void main() { ...@@ -54,6 +55,51 @@ void main() {
expect(find.text(helloSnackBar), findsNothing); expect(find.text(helloSnackBar), findsNothing);
}); });
testWidgets('SnackBar control test - ScaffoldMessenger', (WidgetTester tester) async {
const String helloSnackBar = 'Hello SnackBar';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(helloSnackBar),
duration: Duration(seconds: 2),
));
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 100.0,
width: 100.0,
key: tapTarget,
),
);
}
),
),
));
expect(find.text(helloSnackBar), findsNothing);
await tester.tap(find.byKey(tapTarget));
expect(find.text(helloSnackBar), findsNothing);
await tester.pump(); // schedule animation
expect(find.text(helloSnackBar), findsOneWidget);
await tester.pump(); // begin animation
expect(find.text(helloSnackBar), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 0.75s // animation last frame; two second timer starts here
expect(find.text(helloSnackBar), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 1.50s
expect(find.text(helloSnackBar), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 2.25s
expect(find.text(helloSnackBar), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 3.00s // timer triggers to dismiss snackbar, reverse animation is scheduled
await tester.pump(); // begin animation
expect(find.text(helloSnackBar), findsOneWidget); // frame 0 of dismiss animation
await tester.pump(const Duration(milliseconds: 750)); // 3.75s // last frame of animation, snackbar removed from build
expect(find.text(helloSnackBar), findsNothing);
});
testWidgets('SnackBar twice test', (WidgetTester tester) async { testWidgets('SnackBar twice test', (WidgetTester tester) async {
int snackBarCount = 0; int snackBarCount = 0;
const Key tapTarget = Key('tap-target'); const Key tapTarget = Key('tap-target');
...@@ -129,6 +175,81 @@ void main() { ...@@ -129,6 +175,81 @@ void main() {
expect(find.text('bar2'), findsNothing); expect(find.text('bar2'), findsNothing);
}); });
testWidgets('SnackBar twice test - ScaffoldMessenger', (WidgetTester tester) async {
int snackBarCount = 0;
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
snackBarCount += 1;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('bar$snackBarCount'),
duration: const Duration(seconds: 2),
));
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 100.0,
width: 100.0,
key: tapTarget,
),
);
}
),
),
));
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsNothing);
await tester.tap(find.byKey(tapTarget)); // queue bar1
await tester.tap(find.byKey(tapTarget)); // queue bar2
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsNothing);
await tester.pump(); // schedule animation for bar1
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(); // begin animation
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 0.75s // animation last frame; two second timer starts here
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 1.50s
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 2.25s
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 3.00s // timer triggers to dismiss snackbar, reverse animation is scheduled
await tester.pump(); // begin animation
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 3.75s // last frame of animation, snackbar removed from build, new snack bar put in its place
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(); // begin animation
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 4.50s // animation last frame; two second timer starts here
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 5.25s
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 6.00s
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 6.75s // timer triggers to dismiss snackbar, reverse animation is scheduled
await tester.pump(); // begin animation
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 7.50s // last frame of animation, snackbar removed from build, new snack bar put in its place
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsNothing);
});
testWidgets('SnackBar cancel test', (WidgetTester tester) async { testWidgets('SnackBar cancel test', (WidgetTester tester) async {
int snackBarCount = 0; int snackBarCount = 0;
const Key tapTarget = Key('tap-target'); const Key tapTarget = Key('tap-target');
...@@ -215,6 +336,92 @@ void main() { ...@@ -215,6 +336,92 @@ void main() {
expect(find.text('bar2'), findsNothing); expect(find.text('bar2'), findsNothing);
}); });
testWidgets('SnackBar cancel test - ScaffoldMessenger', (WidgetTester tester) async {
int snackBarCount = 0;
const Key tapTarget = Key('tap-target');
int time;
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> lastController;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
snackBarCount += 1;
lastController = ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('bar$snackBarCount'),
duration: Duration(seconds: time),
));
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 100.0,
width: 100.0,
key: tapTarget,
),
);
}
),
),
));
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsNothing);
time = 1000;
await tester.tap(find.byKey(tapTarget)); // queue bar1
final ScaffoldFeatureController<SnackBar, SnackBarClosedReason> firstController = lastController;
time = 2;
await tester.tap(find.byKey(tapTarget)); // queue bar2
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsNothing);
await tester.pump(); // schedule animation for bar1
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(); // begin animation
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 0.75s // animation last frame; two second timer starts here
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 1.50s
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 2.25s
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 10000)); // 12.25s
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
firstController.close(); // snackbar is manually dismissed
await tester.pump(const Duration(milliseconds: 750)); // 13.00s // reverse animation is scheduled
await tester.pump(); // begin animation
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 13.75s // last frame of animation, snackbar removed from build, new snack bar put in its place
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(); // begin animation
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 14.50s // animation last frame; two second timer starts here
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 15.25s
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 16.00s
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 16.75s // timer triggers to dismiss snackbar, reverse animation is scheduled
await tester.pump(); // begin animation
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 17.50s // last frame of animation, snackbar removed from build, new snack bar put in its place
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsNothing);
});
testWidgets('SnackBar dismiss test', (WidgetTester tester) async { testWidgets('SnackBar dismiss test', (WidgetTester tester) async {
int snackBarCount = 0; int snackBarCount = 0;
const Key tapTarget = Key('tap-target'); const Key tapTarget = Key('tap-target');
...@@ -260,6 +467,51 @@ void main() { ...@@ -260,6 +467,51 @@ void main() {
expect(find.text('bar2'), findsOneWidget); expect(find.text('bar2'), findsOneWidget);
}); });
testWidgets('SnackBar dismiss test - ScaffoldMessenger', (WidgetTester tester) async {
int snackBarCount = 0;
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
snackBarCount += 1;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('bar$snackBarCount'),
duration: const Duration(seconds: 2),
));
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 100.0,
width: 100.0,
key: tapTarget,
),
);
}
),
),
));
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsNothing);
await tester.tap(find.byKey(tapTarget)); // queue bar1
await tester.tap(find.byKey(tapTarget)); // queue bar2
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsNothing);
await tester.pump(); // schedule animation for bar1
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(); // begin animation
expect(find.text('bar1'), findsOneWidget);
expect(find.text('bar2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 0.75s // animation last frame; two second timer starts here
await tester.drag(find.text('bar1'), const Offset(0.0, 50.0));
await tester.pump(); // bar1 dismissed, bar2 begins animating
expect(find.text('bar1'), findsNothing);
expect(find.text('bar2'), findsOneWidget);
});
testWidgets('SnackBar cannot be tapped twice', (WidgetTester tester) async { testWidgets('SnackBar cannot be tapped twice', (WidgetTester tester) async {
int tapCount = 0; int tapCount = 0;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
...@@ -299,6 +551,45 @@ void main() { ...@@ -299,6 +551,45 @@ void main() {
expect(tapCount, equals(1)); expect(tapCount, equals(1));
}); });
testWidgets('SnackBar cannot be tapped twice - ScaffoldMessenger', (WidgetTester tester) async {
int tapCount = 0;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: 'ACTION',
onPressed: () {
++tapCount;
},
),
));
},
child: const Text('X'),
);
}
),
),
));
await tester.tap(find.text('X'));
await tester.pump(); // start animation
await tester.pump(const Duration(milliseconds: 750));
expect(tapCount, equals(0));
await tester.tap(find.text('ACTION'));
expect(tapCount, equals(1));
await tester.tap(find.text('ACTION'));
expect(tapCount, equals(1));
await tester.pump();
await tester.tap(find.text('ACTION'));
expect(tapCount, equals(1));
});
testWidgets('Light theme SnackBar has dark background', (WidgetTester tester) async { testWidgets('Light theme SnackBar has dark background', (WidgetTester tester) async {
final ThemeData lightTheme = ThemeData.light(); final ThemeData lightTheme = ThemeData.light();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -467,6 +758,53 @@ void main() { ...@@ -467,6 +758,53 @@ void main() {
); );
}); });
testWidgets('SnackbarBehavior.floating is positioned within safe area - ScaffoldMessenger', (WidgetTester tester) async {
const double viewPadding = 50.0;
const double floatingSnackBarDefaultBottomMargin = 10.0;
await tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: const MediaQueryData(
// Simulate non-safe area.
viewPadding: EdgeInsets.only(bottom: viewPadding),
),
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('I am a snack bar.'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text('X'),
);
}
),
),
),
),
);
await tester.tap(find.text('X'));
await tester.pump(); // Start animation
await tester.pump(const Duration(milliseconds: 750));
final Finder materialFinder = find.descendant(
of: find.byType(SnackBar),
matching: find.byType(Material),
);
final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder);
expect(
snackBarBottomLeft.dy,
// Device height is 600.
600 - viewPadding - floatingSnackBarDefaultBottomMargin,
);
});
testWidgets('Snackbar padding can be customized', (WidgetTester tester) async { testWidgets('Snackbar padding can be customized', (WidgetTester tester) async {
const double padding = 20.0; const double padding = 20.0;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -476,7 +814,7 @@ void main() { ...@@ -476,7 +814,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 +858,7 @@ void main() { ...@@ -520,7 +858,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 +896,7 @@ void main() { ...@@ -558,7 +896,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 +949,7 @@ void main() { ...@@ -611,7 +949,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 +1004,7 @@ void main() { ...@@ -666,7 +1004,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 +1055,7 @@ void main() { ...@@ -717,7 +1055,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 +1109,7 @@ void main() { ...@@ -771,7 +1109,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 +1167,7 @@ void main() { ...@@ -829,7 +1167,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 +1199,18 @@ void main() { ...@@ -861,18 +1199,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 +1255,14 @@ void main() { ...@@ -917,14 +1255,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));
...@@ -979,6 +1317,49 @@ void main() { ...@@ -979,6 +1317,49 @@ void main() {
expect(find.text('ACTION'), findsNothing); expect(find.text('ACTION'), findsNothing);
}); });
testWidgets('accessible navigation behavior with action - ScaffoldMessenger', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(MaterialApp(
home: MediaQuery(
data: const MediaQueryData(accessibleNavigation: true),
child: ScaffoldMessenger(
child: Builder(
builder: (BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('snack'),
duration: const Duration(seconds: 1),
action: SnackBarAction(
label: 'ACTION',
onPressed: () { },
),
));
},
child: const Text('X'),
),
);
}
)
),
),
));
await tester.tap(find.text('X'));
await tester.pump();
// Find action immediately
expect(find.text('ACTION'), findsOneWidget);
// Snackbar doesn't close
await tester.pump(const Duration(seconds: 10));
expect(find.text('ACTION'), findsOneWidget);
await tester.tap(find.text('ACTION'));
await tester.pump();
// Snackbar closes immediately
expect(find.text('ACTION'), findsNothing);
});
testWidgets('contributes dismiss semantics', (WidgetTester tester) async { testWidgets('contributes dismiss semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics(); final SemanticsHandle handle = tester.ensureSemantics();
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
...@@ -988,8 +1369,7 @@ void main() { ...@@ -988,8 +1369,7 @@ void main() {
data: const MediaQueryData(accessibleNavigation: true), data: const MediaQueryData(accessibleNavigation: true),
child: Scaffold( child: Scaffold(
key: scaffoldKey, key: scaffoldKey,
body: Builder( body: Builder(builder: (BuildContext context) {
builder: (BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Scaffold.of(context).showSnackBar(SnackBar( Scaffold.of(context).showSnackBar(SnackBar(
...@@ -1003,10 +1383,52 @@ void main() { ...@@ -1003,10 +1383,52 @@ void main() {
}, },
child: const Text('X'), child: const Text('X'),
); );
}, }),
),
)
));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(tester.getSemantics(find.text('snack')), matchesSemantics(
isLiveRegion: true,
hasDismissAction: true,
hasScrollDownAction: true,
hasScrollUpAction: true,
label: 'snack',
textDirection: TextDirection.ltr,
));
handle.dispose();
});
testWidgets('contributes dismiss semantics - ScaffoldMessenger', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(MaterialApp(
home: MediaQuery(
data: const MediaQueryData(accessibleNavigation: true),
child: ScaffoldMessenger(
child: Builder(builder: (BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('snack'),
duration: const Duration(seconds: 1),
action: SnackBarAction(
label: 'ACTION',
onPressed: () { },
), ),
));
},
child: const Text('X'),
), ),
);
}),
), ),
)
)); ));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -1031,7 +1453,7 @@ void main() { ...@@ -1031,7 +1453,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),
)); ));
}, },
...@@ -1118,6 +1540,54 @@ void main() { ...@@ -1118,6 +1540,54 @@ void main() {
expect(find.text('test'), findsNothing); expect(find.text('test'), findsNothing);
}); });
testWidgets('SnackBar handles updates to accessibleNavigation - ScaffoldMessenger', (WidgetTester tester) async {
Future<void> boilerplate({ bool accessibleNavigation }) {
return tester.pumpWidget(MaterialApp(
home: MediaQuery(
data: MediaQueryData(accessibleNavigation: accessibleNavigation),
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('test'),
action: SnackBarAction(label: 'foo', onPressed: () { }),
));
},
behavior: HitTestBehavior.opaque,
child: const Text('X'),
);
}
),
),
),
));
}
await boilerplate(accessibleNavigation: false);
expect(find.text('test'), findsNothing);
await tester.tap(find.text('X'));
await tester.pump(); // schedule animation
expect(find.text('test'), findsOneWidget);
await tester.pump(); // begin animation
await tester.pump(const Duration(milliseconds: 4750)); // 4.75s
expect(find.text('test'), findsOneWidget);
// Enabled accessible navigation
await boilerplate(accessibleNavigation: true);
await tester.pump(const Duration(milliseconds: 4000)); // 8.75s
await tester.pump();
expect(find.text('test'), findsOneWidget);
// disable accessible navigation
await boilerplate(accessibleNavigation: false);
await tester.pumpAndSettle(const Duration(milliseconds: 5750));
expect(find.text('test'), findsNothing);
});
testWidgets('Snackbar asserts if passed a null duration', (WidgetTester tester) async { testWidgets('Snackbar asserts if passed a null duration', (WidgetTester tester) async {
const Key tapTarget = Key('tap-target'); const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
...@@ -1126,7 +1596,7 @@ void main() { ...@@ -1126,7 +1596,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 +1626,7 @@ void main() { ...@@ -1156,7 +1626,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 +1663,14 @@ void main() { ...@@ -1193,14 +1663,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 +1717,8 @@ void main() { ...@@ -1247,8 +1717,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 +1748,8 @@ void main() { ...@@ -1278,8 +1748,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 +1768,8 @@ void main() { ...@@ -1298,9 +1768,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 +1779,7 @@ void main() { ...@@ -1310,7 +1779,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 +1843,8 @@ void main() { ...@@ -1374,8 +1843,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 +1879,7 @@ void main() { ...@@ -1410,7 +1879,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 +1921,8 @@ void main() { ...@@ -1452,8 +1921,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 +1958,8 @@ void main() { ...@@ -1489,8 +1958,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 +1975,107 @@ void main() { ...@@ -1506,4 +1975,107 @@ void main() {
}, },
); );
}); });
testWidgets('SnackBars hero across transitions when using ScaffoldMessenger', (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);
});
testWidgets('SnackBars cannot be used by the Scaffold and ScaffoldMessenger at the same time', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(),
));
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldMessengerState.showSnackBar(SnackBar(
content: const Text('ScaffoldMessenger'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
behavior: SnackBarBehavior.floating,
));
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
scaffoldState.showSnackBar(SnackBar(
content: const Text('Scaffold'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
behavior: SnackBarBehavior.floating,
));
final List<dynamic> exceptions = <dynamic>[];
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
await tester.pump();
FlutterError.onError = oldHandler;
expect(exceptions.length, 1);
final AssertionError error = exceptions.first as AssertionError;
expect(error.message, contains('Only one API should be used to manage SnackBars.'));
});
} }
...@@ -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: () {}),
...@@ -111,7 +111,7 @@ void main() { ...@@ -111,7 +111,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: () {}),
...@@ -155,7 +155,7 @@ void main() { ...@@ -155,7 +155,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,
...@@ -202,7 +202,7 @@ void main() { ...@@ -202,7 +202,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: () {}),
...@@ -244,7 +244,7 @@ void main() { ...@@ -244,7 +244,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