From afa685a4a61abaf13cb0f8389ec01c598f7973c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Braz=CC=87ewicz?= Date: Mon, 26 Oct 2026 17:34:47 +0100 Subject: [PATCH 1/4] fix(ui): show a snackbar queued before its host mounts _SnackbarStageState synced with its messenger in initState, and when a snackbar was already queued it read MediaQuery.accessibleNavigationOf, which depends on an inherited widget before initState completes and throws in debug. An external messenger outlives its host, so this hit any app whose host is rebuilt while a snackbar waits. Read accessibleNavigation without depending on MediaQuery; it is only consulted when an animation starts. Co-Authored-By: Claude Opus 5.5 --- packages/stream_core_flutter/CHANGELOG.md | 3 ++ .../snackbar/stream_snackbar_messenger.dart | 11 ++++- .../snackbar/stream_snackbar_test.dart | 41 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index 23d6946e..5973e8bb 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -4,6 +4,9 @@ - Fixed `StreamTextInput` ignoring typed text after the widgets around it, such as `leading` and `trailing`, changed while it had focus. +- Fixed `StreamSnackbarHost` and `StreamSnackbarPopup` throwing in debug when + they mount while their messenger already has a snackbar queued, as an + external messenger does when the widget holding the host is rebuilt. ## 0.5.3 diff --git a/packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart b/packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart index ebb58300..5f877800 100644 --- a/packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart +++ b/packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart @@ -734,6 +734,13 @@ class _SnackbarStageState extends State<_SnackbarStage> with SingleTickerProvide void _onControllerChanged() => _syncWithController(); + // Read without depending on MediaQuery: it is only consulted when an + // animation starts, and the first one starts from initState when the + // messenger already has a snackbar queued, where depending on an inherited + // widget is not allowed. + bool get _accessibleNavigation => + context.getInheritedWidgetOfExactType()?.data.accessibleNavigation ?? false; + void _syncWithController() { final next = widget.messenger._queue.isEmpty ? null : widget.messenger._queue.first; @@ -756,7 +763,7 @@ class _SnackbarStageState extends State<_SnackbarStage> with SingleTickerProvide _animation.value = 0; setState(() {}); if (next != null) { - if (MediaQuery.accessibleNavigationOf(context)) { + if (_accessibleNavigation) { _animation.value = 1; } else { _animation.forward(from: 0); @@ -786,7 +793,7 @@ class _SnackbarStageState extends State<_SnackbarStage> with SingleTickerProvide final messenger = widget.messenger; _exiting = true; _timer?.cancel(); - if (MediaQuery.accessibleNavigationOf(context)) { + if (_accessibleNavigation) { _animation.value = 0; } else { await _animation.reverse(); diff --git a/packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart b/packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart index 083d50dd..60e0bb76 100644 --- a/packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart +++ b/packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart @@ -848,6 +848,31 @@ void main() { expect(find.text('Auto-managed'), findsOneWidget); }); + + // An external messenger outlives the popup, so a snackbar can be queued + // while no popup is mounted, such as while the bar holding it is rebuilt. + testWidgets('shows a snackbar queued before it mounts', (tester) async { + final messenger = StreamSnackbarMessenger(); + addTearDown(messenger.dispose); + + messenger.show(StreamSnackbar(message: const Text('Queued'))); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(extensions: [StreamTheme.light()]), + home: Center( + child: StreamSnackbarPopup.withState( + messenger: messenger, + child: const SizedBox(width: 200, height: 60), + ), + ), + ), + ); + await tester.pump(const Duration(milliseconds: 300)); + + expect(tester.takeException(), isNull); + expect(find.text('Queued'), findsOneWidget); + }); }); group('accessibleNavigation', () { @@ -893,6 +918,22 @@ void main() { expect(fadeOf(tester).opacity.value, equals(1.0)); }); + testWidgets('on — a snackbar queued before the host mounts skips animation', (tester) async { + tester.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures( + accessibleNavigation: true, + ); + addTearDown(tester.platformDispatcher.clearAccessibilityFeaturesTestValue); + + final messenger = StreamSnackbarMessenger(); + addTearDown(messenger.dispose); + + messenger.show(StreamSnackbar(message: const Text('Queued'))); + await tester.pumpWidget(hostScaffold(messenger)); + + expect(tester.takeException(), isNull); + expect(fadeOf(tester).opacity.value, equals(1.0)); + }); + testWidgets('on — exit skips animation and resolves controller after one frame', (tester) async { tester.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures( accessibleNavigation: true, From 359743c0b176078d2370d4a1659550e68122264a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Bra=C5=BCewicz?= Date: Tue, 27 Oct 2026 10:00:42 +0100 Subject: [PATCH 2/4] Update packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart Co-authored-by: Sahil Kumar --- .../snackbar/stream_snackbar_messenger.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart b/packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart index 5f877800..2d7a2590 100644 --- a/packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart +++ b/packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart @@ -734,12 +734,11 @@ class _SnackbarStageState extends State<_SnackbarStage> with SingleTickerProvide void _onControllerChanged() => _syncWithController(); - // Read without depending on MediaQuery: it is only consulted when an - // animation starts, and the first one starts from initState when the - // messenger already has a snackbar queued, where depending on an inherited - // widget is not allowed. - bool get _accessibleNavigation => - context.getInheritedWidgetOfExactType()?.data.accessibleNavigation ?? false; + // No dependency on MediaQuery: the first read can come from initState. + bool get _accessibleNavigation { + final mediaQuery = context.getInheritedWidgetOfExactType(); + return mediaQuery?.data.accessibleNavigation ?? false; + } void _syncWithController() { final next = widget.messenger._queue.isEmpty ? null : widget.messenger._queue.first; From 240cd1dd34c7b8f995820980eb8155719de7f519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Bra=C5=BCewicz?= Date: Tue, 27 Oct 2026 10:00:51 +0100 Subject: [PATCH 3/4] Update packages/stream_core_flutter/CHANGELOG.md Co-authored-by: Sahil Kumar --- packages/stream_core_flutter/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index 5973e8bb..5c59059e 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -5,8 +5,8 @@ - Fixed `StreamTextInput` ignoring typed text after the widgets around it, such as `leading` and `trailing`, changed while it had focus. - Fixed `StreamSnackbarHost` and `StreamSnackbarPopup` throwing in debug when - they mount while their messenger already has a snackbar queued, as an - external messenger does when the widget holding the host is rebuilt. + they mount with a snackbar already queued on their messenger, for example + after the widget holding them is rebuilt. ## 0.5.3 From e3ebbe8eb59eb893346551766ef202f105a31e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Bra=C5=BCewicz?= Date: Tue, 27 Oct 2026 10:52:53 +0100 Subject: [PATCH 4/4] Update packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart Co-authored-by: Sahil Kumar --- .../test/components/snackbar/stream_snackbar_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart b/packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart index 60e0bb76..fc9addc5 100644 --- a/packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart +++ b/packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart @@ -849,8 +849,6 @@ void main() { expect(find.text('Auto-managed'), findsOneWidget); }); - // An external messenger outlives the popup, so a snackbar can be queued - // while no popup is mounted, such as while the bar holding it is rebuilt. testWidgets('shows a snackbar queued before it mounts', (tester) async { final messenger = StreamSnackbarMessenger(); addTearDown(messenger.dispose);