From 28d96aa442392055f5ccd8ca9b38442ef9b8fe5b Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Mon, 20 Jul 2020 13:47:05 -0700 Subject: [PATCH 1/7] Init --- packages/flutter/lib/src/material/app.dart | 159 +++++++++--------- .../flutter/lib/src/material/scaffold.dart | 132 +++++++++++++++ 2 files changed, 213 insertions(+), 78 deletions(-) diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 205a7f34b47bb..afd828099dd04 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -16,6 +16,7 @@ import 'floating_action_button.dart'; import 'icons.dart'; import 'material_localizations.dart'; import 'page.dart'; +import 'scaffold.dart'; import 'theme.dart'; /// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage @@ -607,85 +608,87 @@ class _MaterialAppState extends State { Widget build(BuildContext context) { Widget result = HeroControllerScope( controller: _heroController, - child: WidgetsApp( - key: GlobalObjectKey(this), - navigatorKey: widget.navigatorKey, - navigatorObservers: widget.navigatorObservers, - pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) { - return MaterialPageRoute(settings: settings, builder: builder); - }, - home: widget.home, - routes: widget.routes, - initialRoute: widget.initialRoute, - onGenerateRoute: widget.onGenerateRoute, - onGenerateInitialRoutes: widget.onGenerateInitialRoutes, - onUnknownRoute: widget.onUnknownRoute, - builder: (BuildContext context, Widget child) { - // Use a light theme, dark theme, or fallback theme. - final ThemeMode mode = widget.themeMode ?? ThemeMode.system; - ThemeData theme; - if (widget.darkTheme != null) { - final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context); - if (mode == ThemeMode.dark || - (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark)) { - theme = widget.darkTheme; + child: ScaffoldMessenger( + child: WidgetsApp( + key: GlobalObjectKey(this), + navigatorKey: widget.navigatorKey, + navigatorObservers: widget.navigatorObservers, + pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) { + return MaterialPageRoute(settings: settings, builder: builder); + }, + home: widget.home, + routes: widget.routes, + initialRoute: widget.initialRoute, + onGenerateRoute: widget.onGenerateRoute, + onGenerateInitialRoutes: widget.onGenerateInitialRoutes, + onUnknownRoute: widget.onUnknownRoute, + builder: (BuildContext context, Widget child) { + // Use a light theme, dark theme, or fallback theme. + final ThemeMode mode = widget.themeMode ?? ThemeMode.system; + ThemeData theme; + if (widget.darkTheme != null) { + final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context); + if (mode == ThemeMode.dark || + (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark)) { + theme = widget.darkTheme; + } } - } - theme ??= widget.theme ?? ThemeData.fallback(); - - 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, - ); - }, - title: widget.title, - onGenerateTitle: widget.onGenerateTitle, - textStyle: _errorTextStyle, - // The color property is always pulled from the light theme, even if dark - // mode is activated. This was done to simplify the technical details - // of switching themes and it was deemed acceptable because this color - // property is only used on old Android OSes to color the app bar in - // Android's switcher UI. - // - // blue is the primary color of the default theme - color: widget.color ?? widget.theme?.primaryColor ?? Colors.blue, - locale: widget.locale, - localizationsDelegates: _localizationsDelegates, - localeResolutionCallback: widget.localeResolutionCallback, - localeListResolutionCallback: widget.localeListResolutionCallback, - supportedLocales: widget.supportedLocales, - showPerformanceOverlay: widget.showPerformanceOverlay, - checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, - checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, - showSemanticsDebugger: widget.showSemanticsDebugger, - debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner, - inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) { - return FloatingActionButton( - child: const Icon(Icons.search), - onPressed: onPressed, - mini: true, - ); - }, - shortcuts: widget.shortcuts, - actions: widget.actions, + theme ??= widget.theme ?? ThemeData.fallback(); + + 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, + ); + }, + title: widget.title, + onGenerateTitle: widget.onGenerateTitle, + textStyle: _errorTextStyle, + // The color property is always pulled from the light theme, even if dark + // mode is activated. This was done to simplify the technical details + // of switching themes and it was deemed acceptable because this color + // property is only used on old Android OSes to color the app bar in + // Android's switcher UI. + // + // blue is the primary color of the default theme + color: widget.color ?? widget.theme?.primaryColor ?? Colors.blue, + locale: widget.locale, + localizationsDelegates: _localizationsDelegates, + localeResolutionCallback: widget.localeResolutionCallback, + localeListResolutionCallback: widget.localeListResolutionCallback, + supportedLocales: widget.supportedLocales, + showPerformanceOverlay: widget.showPerformanceOverlay, + checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, + checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, + showSemanticsDebugger: widget.showSemanticsDebugger, + debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner, + inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) { + return FloatingActionButton( + child: const Icon(Icons.search), + onPressed: onPressed, + mini: true, + ); + }, + shortcuts: widget.shortcuts, + actions: widget.actions, + ), ), ); diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index ce025932de928..91aff1d67ddda 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -61,6 +61,130 @@ enum _ScaffoldSlot { statusBar, } +/// Doc +class ScaffoldMessenger extends StatefulWidget { + /// Doc + 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; + + /// Returns the closest [ScaffoldMessengerState] which encloses the given + /// context. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ScaffoldMessages scaffoldMessenger = ScaffoldMessages.of(context); + /// scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Snack-tastic')); + /// ``` + static ScaffoldMessengerState of(BuildContext context) { + final _ScaffoldMessengerScope scope = context.dependOnInheritedWidgetOfExactType<_ScaffoldMessengerScope>(); + return scope?._scaffoldMessengerState; + } + + @override + ScaffoldMessengerState createState() => ScaffoldMessengerState(); +} + +/// Doc +class ScaffoldMessengerState extends State { + final Set _scaffolds = {}; + final Queue> _snackBars = Queue< + ScaffoldFeatureController>(); + + void _register(ScaffoldState scaffold) { + // Are we in the middle of showing a SnackBar elsewhere? + // If so, present on the new scaffold. + if (!_scaffolds.contains(scaffold) && _snackBars.isNotEmpty) { + _snackBars.add(scaffold.showSnackBar(_snackBars.first._widget)); + } + _scaffolds.add(scaffold); + } + + void _unregister(ScaffoldState scaffold) { + _scaffolds.remove(scaffold); + } + + void _handleSnackBarStatusChange(AnimationStatus status) { + switch (status) { + case AnimationStatus.dismissed: + if (_snackBars.isNotEmpty) + _snackBars.removeFirst(); + // If there is another Scaffold in the stack presenting a SnackBar, we + // want to make sure it has been dismissed too. + hideCurrentSnackBar(); + break; + case AnimationStatus.completed: + case AnimationStatus.forward: + case AnimationStatus.reverse: + break; + } + } + + /// Doc + ScaffoldFeatureController showSnackBar( + SnackBar snackbar) { + for (final ScaffoldState scaffold in _scaffolds) + _snackBars.add(scaffold?.showSnackBar(snackbar)); + return _snackBars.first; + } + + /// Doc + void removeCurrentSnackBar( + { SnackBarClosedReason reason = SnackBarClosedReason.remove }) { + for (final ScaffoldState scaffold in _scaffolds) + scaffold.removeCurrentSnackBar(reason: reason); + } + + /// Doc + void hideCurrentSnackBar( + { SnackBarClosedReason reason = SnackBarClosedReason.hide }) { + for (final ScaffoldState scaffold in _scaffolds) + scaffold.hideCurrentSnackBar(reason: reason); + } + + // TODO(Piinks): state restoration + + // ++ later: + // - openDrawer? + // - openEndDrawer? + // - showMaterialBanner, needs Scaffold support first, https://github.com/flutter/flutter/issues/60024 + // - showBottomSheet? + + @override + Widget build(BuildContext context) { + return _ScaffoldMessengerScope( + scaffoldMessengerState: this, + child: widget.child, + ); + } +} + +class _ScaffoldMessengerScope extends InheritedWidget { + const _ScaffoldMessengerScope({ + Key key, + Widget child, + ScaffoldMessengerState scaffoldMessengerState, + }) : _scaffoldMessengerState = scaffoldMessengerState, + super(key: key, child: child); + + final ScaffoldMessengerState _scaffoldMessengerState; + + /// Doc + ScaffoldMessenger get scaffoldMessenger => _scaffoldMessengerState.widget; + + @override + bool updateShouldNotify(_ScaffoldMessengerScope old) => _scaffoldMessengerState != old._scaffoldMessengerState; +} + /// The geometry of the [Scaffold] after all its contents have been laid out /// except the [FloatingActionButton]. /// @@ -1030,10 +1154,12 @@ class Scaffold extends StatefulWidget { this.drawerEdgeDragWidth, this.drawerEnableOpenDragGesture = true, this.endDrawerEnableOpenDragGesture = true, + this.registerMessenger = true, }) : assert(primary != null), assert(extendBody != null), assert(extendBodyBehindAppBar != null), assert(drawerDragStartBehavior != null), + assert(registerMessenger != null), super(key: key); /// If true, and [bottomNavigationBar] or [persistentFooterButtons] @@ -1347,6 +1473,9 @@ class Scaffold extends StatefulWidget { /// By default, the drag gesture is enabled. final bool endDrawerEnableOpenDragGesture; + /// Doc + final bool registerMessenger; + /// The state from the closest instance of this class that encloses the given context. /// /// {@tool dartpad --template=freeform} @@ -2203,6 +2332,8 @@ class ScaffoldState extends State with TickerProviderStateMixin { } _accessibleNavigation = mediaQuery.accessibleNavigation; _maybeBuildPersistentBottomSheet(); + if (widget.registerMessenger) + ScaffoldMessenger.of(context)?._register(this); super.didChangeDependencies(); } @@ -2220,6 +2351,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { } _floatingActionButtonMoveController.dispose(); _floatingActionButtonVisibilityController.dispose(); + ScaffoldMessenger.of(context)?._unregister(this); super.dispose(); } From 2386690d49e4dc45c0f6a22b446ce541d658386a Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Mon, 20 Jul 2020 15:41:37 -0700 Subject: [PATCH 2/7] Changing completion: --- .../flutter/lib/src/material/scaffold.dart | 83 +++++++++++-------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 91aff1d67ddda..5ac4b96029a96 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -95,64 +95,77 @@ class ScaffoldMessenger extends StatefulWidget { /// Doc class ScaffoldMessengerState extends State { - final Set _scaffolds = {}; - final Queue> _snackBars = Queue< - ScaffoldFeatureController>(); + final LinkedHashSet _scaffolds = LinkedHashSet(); + final Queue _snackBars = Queue(); + final Map>> _snackBarControllers = >>{}; void _register(ScaffoldState scaffold) { // Are we in the middle of showing a SnackBar elsewhere? - // If so, present on the new scaffold. + // If so, present on this newly registered scaffold. if (!_scaffolds.contains(scaffold) && _snackBars.isNotEmpty) { - _snackBars.add(scaffold.showSnackBar(_snackBars.first._widget)); + final SnackBar snackbar = _snackBars.first; + final ScaffoldFeatureController controller = scaffold.showSnackBar(snackbar); + controller.closed.then((SnackBarClosedReason reason) { + _handleSnackBarStatusChange(snackbar, reason); + }); + _snackBarControllers[snackbar.hashCode].add(controller); } _scaffolds.add(scaffold); } void _unregister(ScaffoldState scaffold) { + // TODO(Piinks): What happens if a snackbar is showing while a scaffold is + // disposed and unregistered? How is that completed? _scaffolds.remove(scaffold); } - void _handleSnackBarStatusChange(AnimationStatus status) { - switch (status) { - case AnimationStatus.dismissed: - if (_snackBars.isNotEmpty) - _snackBars.removeFirst(); - // If there is another Scaffold in the stack presenting a SnackBar, we - // want to make sure it has been dismissed too. - hideCurrentSnackBar(); - break; - case AnimationStatus.completed: - case AnimationStatus.forward: - case AnimationStatus.reverse: - break; + void _handleSnackBarStatusChange(SnackBar snackbar, SnackBarClosedReason reason) { + // We may have multiple controllers for the same snackbar across multiple + // Scaffolds, one of them has completed so the rest should be completed + // with the same SnackBarClosedReason. + for (final ScaffoldFeatureController controller in _snackBarControllers[snackbar.hashCode]) { + controller._completer.complete(reason); } + // The SnackBar has been delivered and dismissed, remove along with + // associated controllers. + _snackBarControllers.remove(snackbar.hashCode); + // Make sure we are getting rid of the right one, queue is being respected + assert(snackbar == _snackBars.first); + _snackBars.removeFirst(); } /// Doc - ScaffoldFeatureController showSnackBar( - SnackBar snackbar) { - for (final ScaffoldState scaffold in _scaffolds) - _snackBars.add(scaffold?.showSnackBar(snackbar)); - return _snackBars.first; + ScaffoldFeatureController showSnackBar(SnackBar snackbar) { + // Keep track of the snackbar we are propagating across scaffolds. + _snackBars.add(snackbar); + ScaffoldFeatureController controller; + for (final ScaffoldState scaffold in _scaffolds) { + controller = scaffold.showSnackBar(snackbar); + controller.closed.then((SnackBarClosedReason reason) { + _handleSnackBarStatusChange(snackbar, reason); + }); + _snackBarControllers[snackbar.hashCode].add(controller); + } + // Return the last controller, which is for the most recently registered + // Scaffold. Controllers for all of the Scaffolds will be completed with the + // same SnackBarClosedReason. + return controller; } /// Doc - void removeCurrentSnackBar( - { SnackBarClosedReason reason = SnackBarClosedReason.remove }) { + void removeCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.remove }) { for (final ScaffoldState scaffold in _scaffolds) scaffold.removeCurrentSnackBar(reason: reason); } /// Doc - void hideCurrentSnackBar( - { SnackBarClosedReason reason = SnackBarClosedReason.hide }) { + void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) { for (final ScaffoldState scaffold in _scaffolds) scaffold.hideCurrentSnackBar(reason: reason); } - // TODO(Piinks): state restoration - + // TODO(Piinks): how might state restoration need to be considered? + // Test with mix of global and local snackbars // ++ later: // - openDrawer? // - openEndDrawer? @@ -1787,6 +1800,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { AnimationController _snackBarController; Timer _snackBarTimer; bool _accessibleNavigation; + ScaffoldMessengerState _scaffoldMessenger; /// Shows a [SnackBar] at the bottom of the scaffold. /// @@ -1824,6 +1838,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { /// ``` /// {@end-tool} ScaffoldFeatureController showSnackBar(SnackBar snackbar) { + print('Scaffold.showSnackBar, showing: $snackbar in $this'); _snackBarController ??= SnackBar.createAnimationController(vsync: this) ..addStatusListener(_handleSnackBarStatusChange); if (_snackBars.isEmpty) { @@ -2332,8 +2347,10 @@ class ScaffoldState extends State with TickerProviderStateMixin { } _accessibleNavigation = mediaQuery.accessibleNavigation; _maybeBuildPersistentBottomSheet(); - if (widget.registerMessenger) - ScaffoldMessenger.of(context)?._register(this); + if (widget.registerMessenger) { + _scaffoldMessenger = ScaffoldMessenger.of(context); + _scaffoldMessenger?._register(this); + } super.didChangeDependencies(); } @@ -2351,7 +2368,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { } _floatingActionButtonMoveController.dispose(); _floatingActionButtonVisibilityController.dispose(); - ScaffoldMessenger.of(context)?._unregister(this); + _scaffoldMessenger?._unregister(this); super.dispose(); } From a81ececece131a8be4dee771dd5ac4ddd3969caa Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 21 Jul 2020 12:55:42 -0700 Subject: [PATCH 3/7] Pre- removeFromQueue -refactor --- .../flutter/lib/src/material/scaffold.dart | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 5ac4b96029a96..2ce0f11389f32 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -97,7 +97,7 @@ class ScaffoldMessenger extends StatefulWidget { class ScaffoldMessengerState extends State { final LinkedHashSet _scaffolds = LinkedHashSet(); final Queue _snackBars = Queue(); - final Map>> _snackBarControllers = >>{}; + final Map>> _snackBarControllers = >>{}; void _register(ScaffoldState scaffold) { // Are we in the middle of showing a SnackBar elsewhere? @@ -120,17 +120,23 @@ class ScaffoldMessengerState extends State { } void _handleSnackBarStatusChange(SnackBar snackbar, SnackBarClosedReason reason) { - // We may have multiple controllers for the same snackbar across multiple - // Scaffolds, one of them has completed so the rest should be completed - // with the same SnackBarClosedReason. + // We've already completed and popped the finished snackbar, we're here due to + // the subsequent completions, so get out. + if (_snackBars.isEmpty || snackbar != _snackBars.first) + return; + // There may be multiple controllers handling the same snackbar across + // multiple Scaffolds, one of them has completed so the rest should be + // completed with the same SnackBarClosedReason. for (final ScaffoldFeatureController controller in _snackBarControllers[snackbar.hashCode]) { - controller._completer.complete(reason); + if (!controller._completer.isCompleted) { + print('completing with $reason'); + controller._completer.complete(reason); + } } // The SnackBar has been delivered and dismissed, remove along with // associated controllers. _snackBarControllers.remove(snackbar.hashCode); - // Make sure we are getting rid of the right one, queue is being respected - assert(snackbar == _snackBars.first); +// assert(snackbar == _snackBars.first); _snackBars.removeFirst(); } @@ -138,14 +144,17 @@ class ScaffoldMessengerState extends State { ScaffoldFeatureController showSnackBar(SnackBar snackbar) { // Keep track of the snackbar we are propagating across scaffolds. _snackBars.add(snackbar); + final List> controllers = >[]; ScaffoldFeatureController controller; for (final ScaffoldState scaffold in _scaffolds) { controller = scaffold.showSnackBar(snackbar); - controller.closed.then((SnackBarClosedReason reason) { - _handleSnackBarStatusChange(snackbar, reason); - }); - _snackBarControllers[snackbar.hashCode].add(controller); +// controller.closed.then((SnackBarClosedReason reason) { +// _handleSnackBarStatusChange(snackbar, reason); +// print('closed'); +// }); + controllers.add(controller); } + _snackBarControllers[snackbar.hashCode] = controllers; // Return the last controller, which is for the most recently registered // Scaffold. Controllers for all of the Scaffolds will be completed with the // same SnackBarClosedReason. @@ -1853,6 +1862,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { snackbar.withAnimation(_snackBarController, fallbackKey: UniqueKey()), Completer(), () { + print('in completer'); assert(_snackBars.first == controller); hideCurrentSnackBar(reason: SnackBarClosedReason.hide); }, @@ -1906,6 +1916,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { /// /// The closed completer is called after the animation is complete. void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) { + print('hideCurrentSnackBar'); assert(reason != null); if (_snackBars.isEmpty || _snackBarController.status == AnimationStatus.dismissed) return; @@ -2343,6 +2354,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { && !mediaQuery.accessibleNavigation && _snackBarTimer != null && !_snackBarTimer.isActive) { + print('didChangeDependencies'); hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); } _accessibleNavigation = mediaQuery.accessibleNavigation; @@ -2496,6 +2508,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { final MediaQueryData mediaQuery = MediaQuery.of(context); if (mediaQuery.accessibleNavigation && snackBar.action != null) return; + print('build'); hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); }); } From 78f92173f9e57aa352c812041d2df41f1e307b36 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 21 Jul 2020 14:20:12 -0700 Subject: [PATCH 4/7] Pre-_MultiScaffoldSnackBar-refactor --- .../flutter/lib/src/material/scaffold.dart | 113 ++++++++++-------- 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 2ce0f11389f32..d14c4dd320cb8 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -61,6 +61,21 @@ enum _ScaffoldSlot { statusBar, } +/// Doc +class _MultiScaffoldSnackBar { + _MultiScaffoldSnackBar(this.snackbar); + + final SnackBar snackbar; + + Map> get connectedScaffolds => _connectedScaffolds; + final Map> _connectedScaffolds = >{}; + + void addConnectedScaffold(ScaffoldState scaffold, ScaffoldFeatureController controller) { + _connectedScaffolds[scaffold] = controller; + } +} + + /// Doc class ScaffoldMessenger extends StatefulWidget { /// Doc @@ -97,7 +112,7 @@ class ScaffoldMessenger extends StatefulWidget { class ScaffoldMessengerState extends State { final LinkedHashSet _scaffolds = LinkedHashSet(); final Queue _snackBars = Queue(); - final Map>> _snackBarControllers = >>{}; + final Map>> _snackBarControllers = >>{}; void _register(ScaffoldState scaffold) { // Are we in the middle of showing a SnackBar elsewhere? @@ -108,35 +123,39 @@ class ScaffoldMessengerState extends State { controller.closed.then((SnackBarClosedReason reason) { _handleSnackBarStatusChange(snackbar, reason); }); - _snackBarControllers[snackbar.hashCode].add(controller); + _snackBarControllers[snackbar.hashCode][scaffold] = controller; } _scaffolds.add(scaffold); } void _unregister(ScaffoldState scaffold) { - // TODO(Piinks): What happens if a snackbar is showing while a scaffold is - // disposed and unregistered? How is that completed? _scaffolds.remove(scaffold); + // We may also have a reference to this Scaffold for the SnackBars we are + // currently broadcasting. + if (_snackBars.isNotEmpty) { + for (final SnackBar snackbar in _snackBars) { + _snackBarControllers[snackbar.hashCode].remove(scaffold); + } + } } void _handleSnackBarStatusChange(SnackBar snackbar, SnackBarClosedReason reason) { - // We've already completed and popped the finished snackbar, we're here due to - // the subsequent completions, so get out. - if (_snackBars.isEmpty || snackbar != _snackBars.first) + // We've already completed and popped the finished snackbar, we're here due + // to the subsequent completions of propagated snackbars, so get out. + if (_snackBars.isEmpty || snackbar != _snackBars.first) { return; - // There may be multiple controllers handling the same snackbar across - // multiple Scaffolds, one of them has completed so the rest should be - // completed with the same SnackBarClosedReason. - for (final ScaffoldFeatureController controller in _snackBarControllers[snackbar.hashCode]) { - if (!controller._completer.isCompleted) { - print('completing with $reason'); - controller._completer.complete(reason); - } } - // The SnackBar has been delivered and dismissed, remove along with - // associated controllers. - _snackBarControllers.remove(snackbar.hashCode); -// assert(snackbar == _snackBars.first); + // We're respecting the ScaffoldMessenger snackbar queue. + assert(snackbar == _snackBars.first); + + // Dismiss snackbar across Scaffolds/remove from local Scaffold queue if + // not yet presented. + for (final ScaffoldState scaffold in _scaffolds) { + // Get the controller for this SnackBar, for this ScaffoldState. + final ScaffoldFeatureController controller = _snackBarControllers[snackbar.hashCode][scaffold]; + scaffold._removeSnackBar(controller, reason); + } + // This SnackBar is done. _snackBars.removeFirst(); } @@ -144,37 +163,24 @@ class ScaffoldMessengerState extends State { ScaffoldFeatureController showSnackBar(SnackBar snackbar) { // Keep track of the snackbar we are propagating across scaffolds. _snackBars.add(snackbar); - final List> controllers = >[]; ScaffoldFeatureController controller; for (final ScaffoldState scaffold in _scaffolds) { controller = scaffold.showSnackBar(snackbar); -// controller.closed.then((SnackBarClosedReason reason) { -// _handleSnackBarStatusChange(snackbar, reason); -// print('closed'); -// }); - controllers.add(controller); + controller.closed.then((SnackBarClosedReason reason) { + _handleSnackBarStatusChange(snackbar, reason); + }); + print('$_snackBarControllers, $snackbar, ${snackbar.hashCode}, $scaffold, $controller'); + _snackBarControllers[snackbar.hashCode][scaffold] = controller; } - _snackBarControllers[snackbar.hashCode] = controllers; - // Return the last controller, which is for the most recently registered - // Scaffold. Controllers for all of the Scaffolds will be completed with the - // same SnackBarClosedReason. + // Return the last controller, which is from the most recently registered + // Scaffold. Regardless of which we return, the controllers for all of the + // Scaffolds will be completed with the same SnackBarClosedReason once the + // first of the SnackBars completes. return controller; } - /// Doc - void removeCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.remove }) { - for (final ScaffoldState scaffold in _scaffolds) - scaffold.removeCurrentSnackBar(reason: reason); - } - - /// Doc - void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) { - for (final ScaffoldState scaffold in _scaffolds) - scaffold.hideCurrentSnackBar(reason: reason); - } - // TODO(Piinks): how might state restoration need to be considered? - // Test with mix of global and local snackbars + // Test with mix of global and local snackbars (ScaffoldMessengerState.showSnackBar + ScaffoldState.showSnackBar) // ++ later: // - openDrawer? // - openEndDrawer? @@ -200,9 +206,6 @@ class _ScaffoldMessengerScope extends InheritedWidget { final ScaffoldMessengerState _scaffoldMessengerState; - /// Doc - ScaffoldMessenger get scaffoldMessenger => _scaffoldMessengerState.widget; - @override bool updateShouldNotify(_ScaffoldMessengerScope old) => _scaffoldMessengerState != old._scaffoldMessengerState; } @@ -1847,7 +1850,6 @@ class ScaffoldState extends State with TickerProviderStateMixin { /// ``` /// {@end-tool} ScaffoldFeatureController showSnackBar(SnackBar snackbar) { - print('Scaffold.showSnackBar, showing: $snackbar in $this'); _snackBarController ??= SnackBar.createAnimationController(vsync: this) ..addStatusListener(_handleSnackBarStatusChange); if (_snackBars.isEmpty) { @@ -1862,7 +1864,6 @@ class ScaffoldState extends State with TickerProviderStateMixin { snackbar.withAnimation(_snackBarController, fallbackKey: UniqueKey()), Completer(), () { - print('in completer'); assert(_snackBars.first == controller); hideCurrentSnackBar(reason: SnackBarClosedReason.hide); }, @@ -1916,7 +1917,6 @@ class ScaffoldState extends State with TickerProviderStateMixin { /// /// The closed completer is called after the animation is complete. void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) { - print('hideCurrentSnackBar'); assert(reason != null); if (_snackBars.isEmpty || _snackBarController.status == AnimationStatus.dismissed) return; @@ -1936,6 +1936,23 @@ class ScaffoldState extends State with TickerProviderStateMixin { _snackBarTimer = null; } + void _removeSnackBar(ScaffoldFeatureController snackbar, SnackBarClosedReason reason) { + // This one has already completed. + if (snackbar._completer.isCompleted) + return; + // If we are already showing the SnackBar propagated by ScaffoldMessenger, + // remove gracefully. + if (snackbar == _snackBars.first) + hideCurrentSnackBar(reason: reason); + else { + // Even if the SnackBar was not viewed on this Scaffold, we should still + // complete it with the same information as the one that was seen and + // dismissed. + snackbar._completer.complete(reason); + _snackBars.remove(snackbar); + } + } + // PERSISTENT BOTTOM SHEET API From d4377395bcd7c1985966327caa3f2e7787b03e98 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 21 Jul 2020 14:54:52 -0700 Subject: [PATCH 5/7] Possibly viable territory --- .../flutter/lib/src/material/scaffold.dart | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index d14c4dd320cb8..c344b027b9fb1 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -66,12 +66,18 @@ class _MultiScaffoldSnackBar { _MultiScaffoldSnackBar(this.snackbar); final SnackBar snackbar; + final Map> _controllerForScaffold = >{}; - Map> get connectedScaffolds => _connectedScaffolds; - final Map> _connectedScaffolds = >{}; + void addControllerForScaffold(ScaffoldState scaffold, ScaffoldFeatureController controller) { + _controllerForScaffold[scaffold] = controller; + } - void addConnectedScaffold(ScaffoldState scaffold, ScaffoldFeatureController controller) { - _connectedScaffolds[scaffold] = controller; + void removeControllerForScaffold(ScaffoldState scaffold) { + _controllerForScaffold.remove(scaffold); + } + + void dismissFromScaffold(ScaffoldState scaffold, SnackBarClosedReason reason) { + scaffold._removeSnackBar(_controllerForScaffold[scaffold], reason); } } @@ -111,19 +117,18 @@ class ScaffoldMessenger extends StatefulWidget { /// Doc class ScaffoldMessengerState extends State { final LinkedHashSet _scaffolds = LinkedHashSet(); - final Queue _snackBars = Queue(); - final Map>> _snackBarControllers = >>{}; + final Queue<_MultiScaffoldSnackBar> _multiSnackBars = Queue<_MultiScaffoldSnackBar>(); void _register(ScaffoldState scaffold) { - // Are we in the middle of showing a SnackBar elsewhere? - // If so, present on this newly registered scaffold. - if (!_scaffolds.contains(scaffold) && _snackBars.isNotEmpty) { - final SnackBar snackbar = _snackBars.first; - final ScaffoldFeatureController controller = scaffold.showSnackBar(snackbar); - controller.closed.then((SnackBarClosedReason reason) { - _handleSnackBarStatusChange(snackbar, reason); - }); - _snackBarControllers[snackbar.hashCode][scaffold] = controller; + // Do we have SnackBars to send to this new Scaffold's queue? + if (!_scaffolds.contains(scaffold) && _multiSnackBars.isNotEmpty) { + for (final _MultiScaffoldSnackBar multiSnackBar in _multiSnackBars) { + final ScaffoldFeatureController controller = scaffold.showSnackBar(multiSnackBar.snackbar); + controller.closed.then((SnackBarClosedReason reason) { + _handleSnackBarStatusChange(multiSnackBar.snackbar, reason); + }); + multiSnackBar.addControllerForScaffold(scaffold, controller); + } } _scaffolds.add(scaffold); } @@ -132,9 +137,9 @@ class ScaffoldMessengerState extends State { _scaffolds.remove(scaffold); // We may also have a reference to this Scaffold for the SnackBars we are // currently broadcasting. - if (_snackBars.isNotEmpty) { - for (final SnackBar snackbar in _snackBars) { - _snackBarControllers[snackbar.hashCode].remove(scaffold); + if (_multiSnackBars.isNotEmpty) { + for (final _MultiScaffoldSnackBar multiSnackbar in _multiSnackBars) { + multiSnackbar.removeControllerForScaffold(scaffold); } } } @@ -142,36 +147,33 @@ class ScaffoldMessengerState extends State { void _handleSnackBarStatusChange(SnackBar snackbar, SnackBarClosedReason reason) { // We've already completed and popped the finished snackbar, we're here due // to the subsequent completions of propagated snackbars, so get out. - if (_snackBars.isEmpty || snackbar != _snackBars.first) { + if (_multiSnackBars.isEmpty || snackbar != _multiSnackBars.first.snackbar) { return; } // We're respecting the ScaffoldMessenger snackbar queue. - assert(snackbar == _snackBars.first); + assert(snackbar == _multiSnackBars.first.snackbar); // Dismiss snackbar across Scaffolds/remove from local Scaffold queue if // not yet presented. for (final ScaffoldState scaffold in _scaffolds) { - // Get the controller for this SnackBar, for this ScaffoldState. - final ScaffoldFeatureController controller = _snackBarControllers[snackbar.hashCode][scaffold]; - scaffold._removeSnackBar(controller, reason); + _multiSnackBars.first.dismissFromScaffold(scaffold, reason); } // This SnackBar is done. - _snackBars.removeFirst(); + _multiSnackBars.removeFirst(); } /// Doc ScaffoldFeatureController showSnackBar(SnackBar snackbar) { - // Keep track of the snackbar we are propagating across scaffolds. - _snackBars.add(snackbar); + final _MultiScaffoldSnackBar _multiSnackBar = _MultiScaffoldSnackBar(snackbar); ScaffoldFeatureController controller; for (final ScaffoldState scaffold in _scaffolds) { controller = scaffold.showSnackBar(snackbar); controller.closed.then((SnackBarClosedReason reason) { _handleSnackBarStatusChange(snackbar, reason); }); - print('$_snackBarControllers, $snackbar, ${snackbar.hashCode}, $scaffold, $controller'); - _snackBarControllers[snackbar.hashCode][scaffold] = controller; + _multiSnackBar.addControllerForScaffold(scaffold, controller); } + _multiSnackBars.add(_multiSnackBar); // Return the last controller, which is from the most recently registered // Scaffold. Regardless of which we return, the controllers for all of the // Scaffolds will be completed with the same SnackBarClosedReason once the @@ -2371,7 +2373,6 @@ class ScaffoldState extends State with TickerProviderStateMixin { && !mediaQuery.accessibleNavigation && _snackBarTimer != null && !_snackBarTimer.isActive) { - print('didChangeDependencies'); hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); } _accessibleNavigation = mediaQuery.accessibleNavigation; @@ -2525,7 +2526,6 @@ class ScaffoldState extends State with TickerProviderStateMixin { final MediaQueryData mediaQuery = MediaQuery.of(context); if (mediaQuery.accessibleNavigation && snackBar.action != null) return; - print('build'); hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); }); } From 438238435d23752ad0b5fe9c0371089fe82bbe46 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 24 Jul 2020 13:02:43 -0700 Subject: [PATCH 6/7] ++ --- .../flutter/lib/src/material/scaffold.dart | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index c344b027b9fb1..69a5d8c425e0b 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -61,11 +61,12 @@ enum _ScaffoldSlot { statusBar, } -/// Doc +/// Manages the multiple [Scaffold]s that a given [SnackBar] is currently displaying +/// on, along with the associated [ScaffoldFeatureController] for the SnackBar. class _MultiScaffoldSnackBar { - _MultiScaffoldSnackBar(this.snackbar); + _MultiScaffoldSnackBar(this.snackBar); - final SnackBar snackbar; + final SnackBar snackBar; final Map> _controllerForScaffold = >{}; void addControllerForScaffold(ScaffoldState scaffold, ScaffoldFeatureController controller) { @@ -82,9 +83,10 @@ class _MultiScaffoldSnackBar { } -/// Doc +/// Dispatches feature events to registered [Scaffold]s. class ScaffoldMessenger extends StatefulWidget { - /// Doc + /// Creates a widget that provides [Scaffold] feature events to its registered + /// [Scaffold] descendants. const ScaffoldMessenger({ Key key, @required this.child, @@ -102,7 +104,7 @@ class ScaffoldMessenger extends StatefulWidget { /// Typical usage is as follows: /// /// ```dart - /// ScaffoldMessages scaffoldMessenger = ScaffoldMessages.of(context); + /// ScaffoldMessenger scaffoldMessenger = ScaffoldMessenger.of(context); /// scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Snack-tastic')); /// ``` static ScaffoldMessengerState of(BuildContext context) { @@ -114,7 +116,13 @@ class ScaffoldMessenger extends StatefulWidget { ScaffoldMessengerState createState() => ScaffoldMessengerState(); } -/// Doc +/// State associated with a [ScaffoldMessenger] widget. +/// +/// A [ScaffoldMessengerState] object can be used to [showSnackBar] for every +/// registered [Scaffold] that is a descendant of the associated +/// [ScaffoldMessenger]. +/// +/// Typically obtained via [ScaffoldMessenger.of]. class ScaffoldMessengerState extends State { final LinkedHashSet _scaffolds = LinkedHashSet(); final Queue<_MultiScaffoldSnackBar> _multiSnackBars = Queue<_MultiScaffoldSnackBar>(); @@ -123,9 +131,9 @@ class ScaffoldMessengerState extends State { // Do we have SnackBars to send to this new Scaffold's queue? if (!_scaffolds.contains(scaffold) && _multiSnackBars.isNotEmpty) { for (final _MultiScaffoldSnackBar multiSnackBar in _multiSnackBars) { - final ScaffoldFeatureController controller = scaffold.showSnackBar(multiSnackBar.snackbar); + final ScaffoldFeatureController controller = scaffold.showSnackBar(multiSnackBar.snackBar); controller.closed.then((SnackBarClosedReason reason) { - _handleSnackBarStatusChange(multiSnackBar.snackbar, reason); + _handleSnackBarStatusChange(multiSnackBar.snackBar, reason); }); multiSnackBar.addControllerForScaffold(scaffold, controller); } @@ -138,22 +146,22 @@ class ScaffoldMessengerState extends State { // We may also have a reference to this Scaffold for the SnackBars we are // currently broadcasting. if (_multiSnackBars.isNotEmpty) { - for (final _MultiScaffoldSnackBar multiSnackbar in _multiSnackBars) { - multiSnackbar.removeControllerForScaffold(scaffold); + for (final _MultiScaffoldSnackBar multiSnackBar in _multiSnackBars) { + multiSnackBar.removeControllerForScaffold(scaffold); } } } - void _handleSnackBarStatusChange(SnackBar snackbar, SnackBarClosedReason reason) { - // We've already completed and popped the finished snackbar, we're here due - // to the subsequent completions of propagated snackbars, so get out. - if (_multiSnackBars.isEmpty || snackbar != _multiSnackBars.first.snackbar) { + void _handleSnackBarStatusChange(SnackBar snackBar, SnackBarClosedReason reason) { + // We've already completed and popped the finished snackBar, we're here due + // to the subsequent completions of propagated snackBars, so get out. + if (_multiSnackBars.isEmpty || snackBar != _multiSnackBars.first.snackBar) { return; } - // We're respecting the ScaffoldMessenger snackbar queue. - assert(snackbar == _multiSnackBars.first.snackbar); + // We're respecting the ScaffoldMessenger snackBar queue. + assert(snackBar == _multiSnackBars.first.snackBar); - // Dismiss snackbar across Scaffolds/remove from local Scaffold queue if + // Dismiss snackBar across Scaffolds/remove from local Scaffold queue if // not yet presented. for (final ScaffoldState scaffold in _scaffolds) { _multiSnackBars.first.dismissFromScaffold(scaffold, reason); @@ -162,14 +170,17 @@ class ScaffoldMessengerState extends State { _multiSnackBars.removeFirst(); } - /// Doc - ScaffoldFeatureController showSnackBar(SnackBar snackbar) { - final _MultiScaffoldSnackBar _multiSnackBar = _MultiScaffoldSnackBar(snackbar); + /// Creates a [_MultiScaffoldSnackBar] in order to distribute a [SnackBar] + /// notification across all registered [Scaffolds]. + ScaffoldFeatureController showSnackBar(SnackBar snackBar) { + final _MultiScaffoldSnackBar _multiSnackBar = _MultiScaffoldSnackBar(snackBar); ScaffoldFeatureController controller; for (final ScaffoldState scaffold in _scaffolds) { - controller = scaffold.showSnackBar(snackbar); + controller = scaffold.showSnackBar(snackBar); + // Set up handler for when the SnackBar is completed, so that all of the + // other instances of the same SnackBar can be completed. controller.closed.then((SnackBarClosedReason reason) { - _handleSnackBarStatusChange(snackbar, reason); + _handleSnackBarStatusChange(snackBar, reason); }); _multiSnackBar.addControllerForScaffold(scaffold, controller); } @@ -182,11 +193,12 @@ class ScaffoldMessengerState extends State { } // TODO(Piinks): how might state restoration need to be considered? - // Test with mix of global and local snackbars (ScaffoldMessengerState.showSnackBar + ScaffoldState.showSnackBar) + // Test with mix of global and local snackBars (ScaffoldMessengerState.showSnackBar + ScaffoldState.showSnackBar) // ++ later: // - openDrawer? // - openEndDrawer? // - showMaterialBanner, needs Scaffold support first, https://github.com/flutter/flutter/issues/60024 + // - seems most reasonable extension, animated notification from above, wherein SnackBars notify from below. // - showBottomSheet? @override From c45f91abc7114de920858141cef77855911a99d6 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 24 Jul 2020 13:09:37 -0700 Subject: [PATCH 7/7] Tidying --- .../flutter/lib/src/material/scaffold.dart | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 69a5d8c425e0b..4e9b9d4d0e7c4 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -82,7 +82,6 @@ class _MultiScaffoldSnackBar { } } - /// Dispatches feature events to registered [Scaffold]s. class ScaffoldMessenger extends StatefulWidget { /// Creates a widget that provides [Scaffold] feature events to its registered @@ -1193,12 +1192,12 @@ class Scaffold extends StatefulWidget { this.drawerEdgeDragWidth, this.drawerEnableOpenDragGesture = true, this.endDrawerEnableOpenDragGesture = true, - this.registerMessenger = true, + this.registerForMessages = true, }) : assert(primary != null), assert(extendBody != null), assert(extendBodyBehindAppBar != null), assert(drawerDragStartBehavior != null), - assert(registerMessenger != null), + assert(registerForMessages != null), super(key: key); /// If true, and [bottomNavigationBar] or [persistentFooterButtons] @@ -1512,8 +1511,13 @@ class Scaffold extends StatefulWidget { /// By default, the drag gesture is enabled. final bool endDrawerEnableOpenDragGesture; - /// Doc - final bool registerMessenger; + /// Determines if the [Scaffold] should register with a [ScaffoldMessenger], + /// which will dispatch feature events to this [Scaffold]. + /// + /// If nesting [Scaffold]s, set to false to prevent duplicate notifications. + /// + /// Defaults to true, cannot be null. + final bool registerForMessages; /// The state from the closest instance of this class that encloses the given context. /// @@ -1950,20 +1954,20 @@ class ScaffoldState extends State with TickerProviderStateMixin { _snackBarTimer = null; } - void _removeSnackBar(ScaffoldFeatureController snackbar, SnackBarClosedReason reason) { + void _removeSnackBar(ScaffoldFeatureController snackBar, SnackBarClosedReason reason) { // This one has already completed. - if (snackbar._completer.isCompleted) + if (snackBar._completer.isCompleted) return; // If we are already showing the SnackBar propagated by ScaffoldMessenger, // remove gracefully. - if (snackbar == _snackBars.first) + if (snackBar == _snackBars.first) hideCurrentSnackBar(reason: reason); else { // Even if the SnackBar was not viewed on this Scaffold, we should still // complete it with the same information as the one that was seen and // dismissed. - snackbar._completer.complete(reason); - _snackBars.remove(snackbar); + snackBar._completer.complete(reason); + _snackBars.remove(snackBar); } } @@ -2389,7 +2393,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { } _accessibleNavigation = mediaQuery.accessibleNavigation; _maybeBuildPersistentBottomSheet(); - if (widget.registerMessenger) { + if (widget.registerForMessages) { _scaffoldMessenger = ScaffoldMessenger.of(context); _scaffoldMessenger?._register(this); }