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';
import 'icons.dart';
import 'material_localizations.dart';
import 'page.dart';
import 'scaffold.dart' show ScaffoldMessenger, ScaffoldMessengerState;
import 'theme.dart';
/// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage
......@@ -166,6 +167,7 @@ class MaterialApp extends StatefulWidget {
const MaterialApp({
Key? key,
this.navigatorKey,
this.scaffoldMessengerKey,
this.home,
this.routes = const <String, WidgetBuilder>{},
this.initialRoute,
......@@ -214,6 +216,7 @@ class MaterialApp extends StatefulWidget {
/// Creates a [MaterialApp] that uses the [Router] instead of a [Navigator].
const MaterialApp.router({
Key? key,
this.scaffoldMessengerKey,
this.routeInformationProvider,
required this.routeInformationParser,
required this.routerDelegate,
......@@ -263,6 +266,14 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.navigatorKey}
final GlobalKey<NavigatorState>? navigatorKey;
/// A key to use when building the [ScaffoldMessenger].
///
/// If a [scaffoldMessengerKey] is specified, the [ScaffoldMessenger] can be
/// directly manipulated without first obtaining it from a [BuildContext] via
/// [ScaffoldMessenger.of]: from the [scaffoldMessengerKey], use the
/// [GlobalKey.currentState] getter.
final GlobalKey<ScaffoldMessengerState>? scaffoldMessengerKey;
/// {@macro flutter.widgets.widgetsApp.home}
final Widget? home;
......@@ -724,27 +735,30 @@ class _MaterialAppState extends State<MaterialApp> {
}
theme ??= widget.theme ?? ThemeData.light();
return AnimatedTheme(
data: theme,
isMaterialAppTheme: true,
child: widget.builder != null
? Builder(
builder: (BuildContext context) {
// Why are we surrounding a builder with a builder?
//
// The widget.builder may contain code that invokes
// Theme.of(), which should return the theme we selected
// above in AnimatedTheme. However, if we invoke
// widget.builder() directly as the child of AnimatedTheme
// then there is no Context separating them, and the
// widget.builder() will not find the theme. Therefore, we
// surround widget.builder with yet another builder so that
// a context separates them and Theme.of() correctly
// resolves to the theme we passed to AnimatedTheme.
return widget.builder!(context, child);
},
)
: child!,
return ScaffoldMessenger(
key: widget.scaffoldMessengerKey,
child: AnimatedTheme(
data: theme,
isMaterialAppTheme: true,
child: widget.builder != null
? Builder(
builder: (BuildContext context) {
// Why are we surrounding a builder with a builder?
//
// The widget.builder may contain code that invokes
// Theme.of(), which should return the theme we selected
// above in AnimatedTheme. However, if we invoke
// widget.builder() directly as the child of AnimatedTheme
// then there is no Context separating them, and the
// widget.builder() will not find the theme. Therefore, we
// surround widget.builder with yet another builder so that
// a context separates them and Theme.of() correctly
// resolves to the theme we passed to AnimatedTheme.
return widget.builder!(context, child);
},
)
: child!,
)
);
}
......
......@@ -135,7 +135,7 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
/// icon: const Icon(Icons.add_alert),
/// tooltip: 'Show Snackbar',
/// onPressed: () {
/// scaffoldKey.currentState.showSnackBar(snackBar);
/// ScaffoldMessenger.of(context).showSnackBar(snackBar);
/// },
/// ),
/// IconButton(
......
......@@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'scaffold.dart' show Scaffold;
import 'scaffold.dart' show Scaffold, ScaffoldMessenger;
/// Asserts that the given context has a [Material] ancestor.
///
......@@ -123,3 +123,34 @@ bool debugCheckHasScaffold(BuildContext context) {
}());
return true;
}
/// Asserts that the given context has a [ScaffoldMessenger] ancestor.
///
/// Used by various widgets to make sure that they are only used in an
/// appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's build method:
///
/// ```dart
/// assert(debugCheckHasScaffoldMessenger(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasScaffoldMessenger(BuildContext context) {
assert(() {
if (context.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';
import 'button_bar.dart';
import 'colors.dart';
import 'curves.dart';
import 'debug.dart';
import 'divider.dart';
import 'drawer.dart';
import 'flexible_space_bar.dart';
......@@ -59,6 +60,402 @@ enum _ScaffoldSlot {
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
/// except the [FloatingActionButton].
///
......@@ -835,11 +1232,11 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// 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.showSnackBar] and [ScaffoldState.showBottomSheet] functions.
/// [ScaffoldState.showBottomSheet] function.
///
/// {@tool dartpad --template=stateful_widget_material}
/// This example shows a [Scaffold] with a [body] and [FloatingActionButton].
......@@ -916,7 +1313,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: Text('Sample Code'),
/// title: const Text('Sample Code'),
/// ),
/// body: Center(
/// child: Text('You have pressed the button $_count times.'),
......@@ -1008,8 +1405,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// * [BottomNavigationBar], which is a horizontal array of buttons typically
/// shown along the bottom of the app using the [bottomNavigationBar]
/// 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
/// app. A bottom sheet can either be persistent, in which case it is shown
/// using the [ScaffoldState.showBottomSheet] method, or modal, in which case
......@@ -1017,7 +1412,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// * [ScaffoldState], which is the state associated with this widget.
/// * <https://material.io/design/layout/responsive-layout-grid.html>
/// * 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
/// [Scaffold Sample Apps](https://flutter.dev/docs/catalog/samples/Scaffold).
class Scaffold extends StatefulWidget {
......@@ -1389,7 +1783,7 @@ class Scaffold extends StatefulWidget {
/// ),
/// home: Scaffold(
/// body: MyScaffoldBody(),
/// appBar: AppBar(title: Text('Scaffold.of Example')),
/// appBar: AppBar(title: const Text('Scaffold.of Example')),
/// ),
/// color: Colors.white,
/// );
......@@ -1403,12 +1797,30 @@ class Scaffold extends StatefulWidget {
/// Widget build(BuildContext context) {
/// return Center(
/// child: ElevatedButton(
/// child: Text('SHOW A SNACKBAR'),
/// child: const Text('SHOW BOTTOM SHEET'),
/// onPressed: () {
/// Scaffold.of(context).showSnackBar(
/// SnackBar(
/// content: Text('Have a snack!'),
/// ),
/// Scaffold.of(context).showBottomSheet<void>(
/// (BuildContext context) {
/// return Container(
/// alignment: Alignment.center,
/// height: 200,
/// color: Colors.amber,
/// child: Center(
/// child: Column(
/// mainAxisSize: MainAxisSize.min,
/// children: <Widget>[
/// const Text('BottomSheet'),
/// ElevatedButton(
/// child: const Text('Close BottomSheet'),
/// onPressed: () {
/// Navigator.pop(context);
/// },
/// )
/// ],
/// ),
/// ),
/// );
/// },
/// );
/// },
/// ),
......@@ -1429,20 +1841,38 @@ class Scaffold extends StatefulWidget {
/// ```dart
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: Text('Demo')
/// ),
/// appBar: AppBar(title: const Text('Demo')),
/// body: Builder(
/// // Create an inner BuildContext so that the onPressed methods
/// // can refer to the Scaffold with Scaffold.of().
/// builder: (BuildContext context) {
/// return Center(
/// child: ElevatedButton(
/// child: Text('SHOW A SNACKBAR'),
/// child: const Text('SHOW BOTTOM SHEET'),
/// onPressed: () {
/// Scaffold.of(context).showSnackBar(SnackBar(
/// content: Text('Have a snack!'),
/// ));
/// Scaffold.of(context).showBottomSheet<void>(
/// (BuildContext context) {
/// return Container(
/// alignment: Alignment.center,
/// height: 200,
/// color: Colors.amber,
/// child: Center(
/// child: Column(
/// mainAxisSize: MainAxisSize.min,
/// children: <Widget>[
/// const Text('BottomSheet'),
/// ElevatedButton(
/// child: const Text('Close BottomSheet'),
/// onPressed: () {
/// Navigator.pop(context);
/// },
/// )
/// ],
/// ),
/// ),
/// );
/// },
/// );
/// },
/// ),
/// );
......@@ -1559,7 +1989,7 @@ class Scaffold extends StatefulWidget {
/// See also:
///
/// * [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 }) {
assert(registerForUpdates != null);
assert(context != null);
......@@ -1578,8 +2008,8 @@ class Scaffold extends StatefulWidget {
/// State for a [Scaffold].
///
/// Can display [SnackBar]s and [BottomSheet]s. Retrieve a [ScaffoldState] from
/// the current [BuildContext] using [Scaffold.of].
/// Can display [BottomSheet]s. Retrieve a [ScaffoldState] from the current
/// [BuildContext] using [Scaffold.of].
class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
// DRAWER API
......@@ -1669,13 +2099,15 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
}
// SNACKBAR API
final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
AnimationController? _snackBarController;
Timer? _snackBarTimer;
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
/// called while another snack bar is already visible, the given snack bar
......@@ -1684,12 +2116,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
///
/// 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].
/// To remove the [SnackBar] with an exit animation, use
/// [ScaffoldMessengerState.hideCurrentSnackBar] or call
/// [ScaffoldFeatureController.close] on the returned [ScaffoldFeatureController].
/// 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}
///
......@@ -1699,17 +2133,22 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// Widget build(BuildContext context) {
/// return OutlinedButton(
/// onPressed: () {
/// Scaffold.of(context).showSnackBar(
/// ScaffoldMessenger.of(context).showSnackBar(
/// 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}
///
/// 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) {
_snackBarController ??= SnackBar.createAnimationController(vsync: this)
..addStatusListener(_handleSnackBarStatusChange);
......@@ -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
/// 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 }) {
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)
return;
final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
......@@ -1774,11 +2240,38 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_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.
///
/// 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 }) {
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)
return;
final MediaQueryData mediaQuery = MediaQuery.of(context)!;
......@@ -1797,6 +2290,18 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_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
......@@ -2010,7 +2515,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// const Text('BottomSheet'),
/// ElevatedButton(
/// child: const Text('Close BottomSheet'),
/// onPressed: () => Navigator.pop(context),
/// onPressed: () {
/// Navigator.pop(context);
/// }
/// )
/// ],
/// ),
......@@ -2198,6 +2705,19 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
@override
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)!;
// If we transition from accessible navigation to non-accessible navigation
// and there is a SnackBar that would have timed out that has already
......@@ -2210,15 +2730,18 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
}
_accessibleNavigation = mediaQuery.accessibleNavigation;
_maybeBuildPersistentBottomSheet();
super.didChangeDependencies();
}
@override
void dispose() {
// TODO(Piinks): Remove old SnackBar API after migrating ScaffoldMessenger
_snackBarController?.dispose();
_snackBarTimer?.cancel();
_snackBarTimer = null;
_geometryNotifier.dispose();
for (final _StandardBottomSheet bottomSheet in _dismissedBottomSheets) {
bottomSheet.animationController.dispose();
......@@ -2228,6 +2751,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
}
_floatingActionButtonMoveController.dispose();
_floatingActionButtonVisibilityController.dispose();
_scaffoldMessenger?._unregister(this);
super.dispose();
}
......@@ -2341,8 +2865,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
final MediaQueryData mediaQuery = MediaQuery.of(context)!;
final ThemeData themeData = Theme.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) {
final ModalRoute<dynamic>? route = ModalRoute.of(context);
if (route == null || route.isCurrent) {
......@@ -2417,6 +2942,38 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
bool isSnackBarFloating = false;
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) {
final SnackBarBehavior snackBarBehavior = _snackBars.first._widget.behavior
?? themeData.snackBarTheme.behavior
......@@ -2590,7 +3147,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// 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> {
const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState);
final T _widget;
......
......@@ -32,7 +32,7 @@ const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlo
/// Specify how a [SnackBar] was closed.
///
/// The [ScaffoldState.showSnackBar] function returns a
/// The [ScaffoldMessengerState.showSnackBar] function returns a
/// [ScaffoldFeatureController]. The value of the controller's closed property
/// is a Future that resolves to a SnackBarClosedReason. Applications that need
/// to know how a snackbar was closed can use this value.
......@@ -40,7 +40,7 @@ const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlo
/// Example:
///
/// ```dart
/// Scaffold.of(context).showSnackBar(
/// ScaffoldMessenger.of(context).showSnackBar(
/// SnackBar( ... )
/// ).closed.then((SnackBarClosedReason reason) {
/// ...
......@@ -57,10 +57,10 @@ enum SnackBarClosedReason {
swipe,
/// The snack bar was closed by the [ScaffoldFeatureController] close callback
/// or by calling [ScaffoldState.hideCurrentSnackBar] directly.
/// or by calling [ScaffoldMessengerState.hideCurrentSnackBar] directly.
hide,
/// The snack bar was closed by an call to [ScaffoldState.removeCurrentSnackBar].
/// The snack bar was closed by an call to [ScaffoldMessengerState.removeCurrentSnackBar].
remove,
/// The snack bar was closed because its timer expired.
......@@ -150,8 +150,8 @@ class _SnackBarActionState extends State<SnackBarAction> {
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=zpO6n_oZWw0}
///
/// To display a snack bar, call `Scaffold.of(context).showSnackBar()`, passing
/// an instance of [SnackBar] that describes the message.
/// To display a snack bar, call `ScaffoldMessenger.of(context).showSnackBar()`,
/// passing an instance of [SnackBar] that describes the message.
///
/// To control how long the [SnackBar] remains visible, specify a [duration].
///
......@@ -160,11 +160,11 @@ class _SnackBarActionState extends State<SnackBarAction> {
///
/// See also:
///
/// * [Scaffold.of], to obtain the current [ScaffoldState], which manages the
/// display and animation of snack bars.
/// * [ScaffoldState.showSnackBar], which displays a [SnackBar].
/// * [ScaffoldState.removeCurrentSnackBar], which abruptly hides the currently
/// displayed snack bar, if any, and allows the next to be displayed.
/// * [ScaffoldMessenger.of], to obtain the current [ScaffoldMessengerState],
/// which manages the display and animation of snack bars.
/// * [ScaffoldMessengerState.showSnackBar], which displays a [SnackBar].
/// * [ScaffoldMessengerState.removeCurrentSnackBar], which abruptly hides the
/// currently displayed snack bar, if any, and allows the next to be displayed.
/// * [SnackBarAction], which is used to specify an [action] button to show
/// on the snack bar.
/// * [SnackBarThemeData], to configure the default property values for
......@@ -293,7 +293,7 @@ class SnackBar extends StatefulWidget {
///
/// See also:
///
/// * [ScaffoldState.removeCurrentSnackBar], which abruptly hides the
/// * [ScaffoldMessengerState.removeCurrentSnackBar], which abruptly hides the
/// currently displayed snack bar, if any, and allows the next to be
/// displayed.
/// * <https://material.io/design/components/snackbars.html>
......@@ -305,7 +305,7 @@ class SnackBar extends StatefulWidget {
/// Called the first time that the snackbar is visible within a [Scaffold].
final VoidCallback? onVisible;
// API for Scaffold.showSnackBar():
// API for ScaffoldMessengerState.showSnackBar():
/// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
static AnimationController createAnimationController({ required TickerProvider vsync }) {
......@@ -342,7 +342,6 @@ class SnackBar extends StatefulWidget {
State<SnackBar> createState() => _SnackBarState();
}
class _SnackBarState extends State<SnackBar> {
bool _wasVisible = false;
......@@ -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
/// class _SliverAnimatedListSampleState extends State<SliverAnimatedListSample> {
/// final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
/// final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
/// final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// ListModel<int> _list;
/// int _selectedItem;
/// int _nextItem; // The next item inserted when the user presses the '+' button.
......@@ -567,7 +568,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// _selectedItem = null;
/// });
/// } else {
/// _scaffoldKey.currentState.showSnackBar(SnackBar(
/// _scaffoldMessengerKey.currentState.showSnackBar(SnackBar(
/// content: Text(
/// 'Select an item to remove from the list.',
/// style: TextStyle(fontSize: 20),
......@@ -579,6 +580,7 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// scaffoldMessengerKey: _scaffoldMessengerKey,
/// home: Scaffold(
/// key: _scaffoldKey,
/// body: CustomScrollView(
......
......@@ -2112,33 +2112,52 @@ typedef ElementVisitor = void Function(Element element);
/// widget can be used: the build context passed to the [Builder.builder]
/// callback will be that of the [Builder] itself.
///
/// For example, in the following snippet, the [ScaffoldState.showSnackBar]
/// For example, in the following snippet, the [ScaffoldState.showBottomSheet]
/// method is called on the [Scaffold] widget that the build method itself
/// creates. If a [Builder] had not been used, and instead the `context`
/// argument of the build method itself had been used, no [Scaffold] would have
/// been found, and the [Scaffold.of] function would have returned null.
///
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// // here, Scaffold.of(context) returns null
/// return Scaffold(
/// appBar: AppBar(title: Text('Demo')),
/// body: Builder(
/// builder: (BuildContext context) {
/// return TextButton(
/// child: Text('BUTTON'),
/// onPressed: () {
/// // here, Scaffold.of(context) returns the locally created Scaffold
/// Scaffold.of(context).showSnackBar(SnackBar(
/// content: Text('Hello.')
/// ));
/// }
/// );
/// }
/// )
/// );
/// }
/// @override
/// Widget build(BuildContext context) {
/// // here, Scaffold.of(context) returns null
/// return Scaffold(
/// appBar: const AppBar(title: Text('Demo')),
/// body: Builder(
/// builder: (BuildContext context) {
/// return TextButton(
/// child: const Text('BUTTON'),
/// onPressed: () {
/// Scaffold.of(context).showBottomSheet<void>(
/// (BuildContext context) {
/// return Container(
/// alignment: Alignment.center,
/// height: 200,
/// color: Colors.amber,
/// child: Center(
/// child: Column(
/// mainAxisSize: MainAxisSize.min,
/// children: <Widget>[
/// const Text('BottomSheet'),
/// ElevatedButton(
/// child: const Text('Close BottomSheet'),
/// onPressed: () {
/// Navigator.pop(context),
/// },
/// )
/// ],
/// ),
/// ),
/// );
/// },
/// );
/// },
/// );
/// },
/// )
/// );
/// }
/// ```
///
/// The [BuildContext] for a particular widget can change location over time as
......
......@@ -168,6 +168,8 @@ void main() {
' _InheritedTheme\n'
' Theme\n'
' AnimatedTheme\n'
' _ScaffoldMessengerScope\n'
' ScaffoldMessenger\n'
' Builder\n'
' DefaultTextStyle\n'
' CustomPaint\n'
......@@ -204,4 +206,83 @@ void main() {
' or WidgetsApp widget at the top of your application widget tree.\n',
));
});
testWidgets('debugCheckHasScaffoldMessenger control test', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
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() {
builder: (BuildContext context) {
return FloatingActionButton(
onPressed: () {
Scaffold.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Snacky!')),
);
},
......
......@@ -2034,6 +2034,117 @@ void main() {
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 {
......
......@@ -4,6 +4,7 @@
// @dart = 2.8
import 'package:flutter/foundation.dart' show FlutterExceptionHandler;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -54,6 +55,51 @@ void main() {
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 {
int snackBarCount = 0;
const Key tapTarget = Key('tap-target');
......@@ -129,6 +175,81 @@ void main() {
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 {
int snackBarCount = 0;
const Key tapTarget = Key('tap-target');
......@@ -215,6 +336,92 @@ void main() {
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 {
int snackBarCount = 0;
const Key tapTarget = Key('tap-target');
......@@ -260,7 +467,91 @@ void main() {
expect(find.text('bar2'), findsOneWidget);
});
testWidgets('SnackBar cannot be tapped twice', (WidgetTester tester) async {
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 {
int tapCount = 0;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.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('SnackBar cannot be tapped twice - ScaffoldMessenger', (WidgetTester tester) async {
int tapCount = 0;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
......@@ -268,7 +559,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
......@@ -306,23 +597,23 @@ void main() {
theme: lightTheme,
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(
SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: 'ACTION',
onPressed: () { },
),
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(
SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: 'ACTION',
onPressed: () { },
),
);
},
child: const Text('X'),
);
}
),
);
},
child: const Text('X'),
);
}
),
),
),
......@@ -333,7 +624,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 750));
final RenderPhysicalModel renderModel = tester.renderObject(
find.widgetWithText(Material, 'I am a snack bar.').first
find.widgetWithText(Material, 'I am a snack bar.').first
);
// There is a somewhat complicated background color calculation based
// off of the surface color. For the default light theme it
......@@ -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 {
const double padding = 20.0;
await tester.pumpWidget(
......@@ -476,7 +814,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('I am a snack bar.'),
padding: EdgeInsets.all(padding),
......@@ -520,7 +858,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('I am a snack bar.'),
width: width,
......@@ -558,7 +896,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
......@@ -611,7 +949,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () { }),
......@@ -666,7 +1004,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -717,7 +1055,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -771,7 +1109,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -829,7 +1167,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -861,18 +1199,18 @@ void main() {
});
testWidgets('SnackBarClosedReason', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
bool actionPressed = false;
SnackBarClosedReason closedReason;
await tester.pumpWidget(MaterialApp(
scaffoldMessengerKey: scaffoldMessengerKey,
home: Scaffold(
key: scaffoldKey,
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('snack'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
......@@ -917,14 +1255,14 @@ void main() {
// Pop up the snack bar and then remove it.
await tester.tap(find.text('X'));
await tester.pump(const Duration(milliseconds: 750));
scaffoldKey.currentState.removeCurrentSnackBar();
scaffoldMessengerKey.currentState.removeCurrentSnackBar();
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(closedReason, equals(SnackBarClosedReason.remove));
// Pop up the snack bar and then hide it.
await tester.tap(find.text('X'));
await tester.pump(const Duration(milliseconds: 750));
scaffoldKey.currentState.hideCurrentSnackBar();
scaffoldMessengerKey.currentState.hideCurrentSnackBar();
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(closedReason, equals(SnackBarClosedReason.hide));
......@@ -979,34 +1317,118 @@ void main() {
expect(find.text('ACTION'), findsNothing);
});
testWidgets('contributes dismiss semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
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: Scaffold(
home: MediaQuery(
data: const MediaQueryData(accessibleNavigation: true),
child: ScaffoldMessenger(
child: Builder(
builder: (BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: const Text('snack'),
duration: const Duration(seconds: 1),
action: SnackBarAction(
label: 'ACTION',
onPressed: () { },
),
));
},
child: const Text('X'),
);
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 {
final SemanticsHandle handle = tester.ensureSemantics();
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(MaterialApp(
home: MediaQuery(
data: const MediaQueryData(accessibleNavigation: true),
child: Scaffold(
key: scaffoldKey,
body: Builder(builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.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.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.pumpAndSettle();
......@@ -1031,7 +1453,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(const SnackBar(
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(helloSnackBar),
));
},
......@@ -1071,6 +1493,54 @@ void main() {
});
testWidgets('SnackBar handles updates to accessibleNavigation', (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: () {
Scaffold.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 handles updates to accessibleNavigation - ScaffoldMessenger', (WidgetTester tester) async {
Future<void> boilerplate({ bool accessibleNavigation }) {
return tester.pumpWidget(MaterialApp(
home: MediaQuery(
......@@ -1080,7 +1550,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('test'),
action: SnackBarAction(label: 'foo', onPressed: () { }),
));
......@@ -1126,7 +1596,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(nonconst('hello')),
duration: null,
));
......@@ -1156,7 +1626,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('hello'),
duration: const Duration(seconds: 1),
onVisible: () {
......@@ -1193,14 +1663,14 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('hello'),
duration: const Duration(seconds: 1),
onVisible: () {
called += 1;
},
));
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('hello 2'),
duration: const Duration(seconds: 1),
onVisible: () {
......@@ -1247,8 +1717,8 @@ void main() {
),
);
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
scaffoldState.showSnackBar(snackBar);
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldMessengerState.showSnackBar(snackBar);
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
......@@ -1278,8 +1748,8 @@ void main() {
),
);
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
scaffoldState.showSnackBar(snackBar);
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldMessengerState.showSnackBar(snackBar);
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
......@@ -1298,9 +1768,8 @@ void main() {
testWidgets(
'Padding of $behavior is not consumed by viewInsets',
(WidgetTester tester) async {
final Widget child = Directionality(
textDirection: TextDirection.ltr,
child: Scaffold(
final Widget child = MaterialApp(
home: Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.send),
......@@ -1310,7 +1779,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
......@@ -1374,8 +1843,8 @@ void main() {
),
);
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
scaffoldState.showSnackBar(
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldMessengerState.showSnackBar(
const SnackBar(
content: Text('Snackbar text'),
behavior: SnackBarBehavior.fixed,
......@@ -1410,7 +1879,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -1452,8 +1921,8 @@ void main() {
),
);
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
scaffoldState.showSnackBar(
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldMessengerState.showSnackBar(
const SnackBar(
content: Text('SnackBar text'),
behavior: SnackBarBehavior.fixed,
......@@ -1489,8 +1958,8 @@ void main() {
),
);
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
scaffoldState.showSnackBar(
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldMessengerState.showSnackBar(
const SnackBar(
content: Text('SnackBar text'),
behavior: SnackBarBehavior.floating,
......@@ -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() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text(text),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -111,7 +111,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text(text),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: action, onPressed: () {}),
......@@ -155,7 +155,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
......@@ -202,7 +202,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......@@ -244,7 +244,7 @@ void main() {
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment