Repository navigation
Conversation
A snackbar shown through StreamSnackbarScope now shows inside the StreamScaffold, resting above its bottom slot instead of covering it. Adds StreamScaffold.bottomAccessory, a widget that floats just above bottom and that snackbars also rest above. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
Adds a Snackbars use-case under Scaffold, with knobs for the bottom bar style and the bottom accessory, and a golden matrix of a snackbar resting above a docked bottom, a floating bottom and a bottom accessory. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…amScaffold A StreamScaffold now shows scope snackbars only while its route is current, so a plain page or a dialog pushed on top shows them at the scope's bottom edge instead of hiding them inside the covered page. Also from review: a scope snackbar's action and dismiss act on the scope's messenger, a default StreamSnackbarPopup only defers to a scaffold of its own scope, and the popup docs no longer link the internal surface. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nackbars Drops the route tracking and the StreamSnackbarPopup deferral. Every top-level StreamScaffold shows the scope's snackbar above its bottom slots, as Flutter's Scaffold does; StreamSnackbarPopup behaves as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…senger Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… pops Also drops the snackbar library's doc-only import of the scaffold and spells out the scope fallback in the changelog entry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #212 +/- ##
==========================================
+ Coverage 71.99% 72.72% +0.72%
==========================================
Files 216 216
Lines 9067 9309 +242
==========================================
+ Hits 6528 6770 +242
Misses 2539 2539 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- Show the scope's snackbar in the frame the last StreamScaffold leaves: surfaces leave on deactivate, and the scope's copy is gated at paint time instead of a post-frame rebuild. - Keep the scope's hidden copy out of tickers, focus and semantics. - Strip vertical padding from the snackbar and bottom accessory slots. - Finish a leaving snackbar when the scope's messenger is swapped or the scope is removed mid-exit. - Document where snackbars show without a StreamScaffold and with resizeToAvoidBottomInset: false; mark StreamSnackbarSurface @internal. - Exact-position tests, new placement cases, and a floating-bottom plus accessory golden scenario. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In debug, a StreamScaffold whose bottom and bottomAccessory leave no room for a snackbar now throws "StreamSnackbar presented off screen." instead of placing it above the top of the body. Also splits the scaffold snackbar tests into placement, surface and scope-messenger files, and tightens the StreamSnackbarScope and StreamSnackbarSurface docs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eds it The accessory slot had no user yet, and how the composer's own snackbar popup should interact with it is still open. Snackbars from the scope keep resting above the scaffold's bottom. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
renefloor
left a comment
There was a problem hiding this comment.
Reviewed with the pr-review-toolkit agents (code, tests, comments). The overall design holds up: registration/reparenting, ticker ownership on the scope, the TickerCanceled/microtask exit handling and the paint-time hiding of the scope's copy all check out. Inline comments cover the two bugs and the doc nits. The test gaps are listed here:
Missing tests
- One
StreamScaffoldpage pushed over another (channel list → channel): one painted copy, one semantics node, placed against the top page's bottom, and back on the lower page's bottom after a pop. - The visible scaffold changes during the enter animation: the timeout should fire once, ~5 s after
show. - Swipe through a scaffold copy:
closed == swipe, the next queued snackbar shows, and no Dismissible error (two Dismissibles now shareObjectKey(hosted)). - Nested scopes with a scaffold at each level (chat's per-channel scope), and body
State/focus surviving a snackbar coming and going.
Weak assertions
- The action test could also check
closed == actionand thatonPressedwas called. - The nested test's inner scaffold could get its own
bottom, to prove that bottom is ignored. - The messenger-swap test could check that the old messenger still has its snackbar queued.
Design points, for discussion
- In debug, the off-screen assert can fire in a cramped but legitimate layout (phone in landscape, keyboard open, tall composer).
- Downstream, stream_chat_flutter sheets/dialogs without a
StreamScaffold(e.g. the attachment picker's permission snackbars) now show their snackbar under the barrier. - Side-by-side top-level scaffolds (tablet master/detail) mean a screen reader announces the snackbar twice.
🤖 Generated with Claude Code
- Drop the old snackbar when the scope's messenger is swapped, even to an empty one, and resolve the action against the current messenger. - Lay the body out with the constraints the region was given. - Fly the snackbar between StreamScaffold pages as one copy; of the surfaces in a route, only the first carries the hero. - Tighten the scope and surface docs and the changelog entry, and add the missing tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@renefloor, on your review: Thanks, this was a thorough pass. In fa14a7e: Tests. All the missing tests are added: scaffold over scaffold (one painted copy, one semantics node, moves on push and back on pop), the timeout firing once ~5 s after Page transitions. Pushing one Off-screen assert. I'd keep it. Flutter's Attachment picker. On v11,
Side-by-side scaffolds. Matches Flutter: each top-level scaffold shows the snackbar. The scope docs now say so, and how to show it once: one outer |
| bool leadsRoute(_StreamSnackbarSurfaceState surface) { | ||
| final route = surface._route; | ||
| if (route == null) return true; | ||
| return _surfaces.every((other) => other._route != route || other._order! >= surface._order!); |
There was a problem hiding this comment.
ModalRoute.of gives only the nearest route, but when the root navigator transitions, Hero._allHeroesFor also collects the Hero on the top page of every nested navigator inside that route, including tabs that aren't visible. So tabs that each have their own Navigator in an IndexedStack (go_router's StatefulShellRoute), each with a top-level StreamScaffold, each lead their own inner route and all carry ObjectKey(hosted). A root push or pop while a snackbar shows then throws:
There are multiple heroes that share the same tag within a subtree … tag: [_HostedSnackbar#1f4f6]
A likely chat case: pin a message in a channel pushed over the tabs, then go straight back. Repro on fa14a7e:
testWidgets('tabs with their own navigators, root push while a snackbar shows', (tester) async {
final navigatorKey = GlobalKey<NavigatorState>();
final tabKey = GlobalKey();
Widget tab(Key? key) => Navigator(
onGenerateRoute: (_) => MaterialPageRoute<void>(
builder: (_) => StreamScaffold(key: key, body: const SizedBox.expand()),
),
);
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigatorKey,
theme: ThemeData(extensions: [StreamTheme.light()]),
builder: (context, child) => StreamSnackbarScope(child: child!),
home: Scaffold(body: IndexedStack(children: [tab(tabKey), tab(null)])),
),
);
StreamSnackbarMessenger.of(tabKey.currentContext!).show(
StreamSnackbar(message: const Text('hi'), duration: const Duration(minutes: 1)),
);
await tester.pumpAndSettle();
navigatorKey.currentState!
.push(MaterialPageRoute<void>(builder: (_) => const StreamScaffold(body: SizedBox.expand())))
.ignore();
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(tester.takeException(), isNull); // fails with the duplicate-tag error
});The assert is debug-only. In release, I'd expect the Hero kept for the tag to be the one in the tab you can't see, so a second copy probably shows during the transition. I haven't run that, though.
Two possible fixes: turn the Hero off where tickers are off (HeroMode(enabled: TickerMode.of(context))), which covers tabs that aren't visible, or decide which surface leads using all the routes above it rather than only the nearest, which also covers nested navigators visible side by side.
🤖 Generated with Claude Code
There was a problem hiding this comment.
Good catch, reproduced on fa14a7e. A root transition also collects the hero on the current page of every nested navigator in the moving page, so grouping by the nearest route wasn't enough. Flutter's own Scaffold hits the same assert in this layout.
In d2216b4, which surface carries the hero now follows the rule Flutter uses to collect heroes. A surface's hero is collected by its own route's transitions, and while its page is the current one in its navigator, by every route above it too. Surfaces collected by the same transition never carry the hero at the same time. Among them, a visible one takes it from a hidden one (an IndexedStack tab, or a go_router branch with its tickers off), and otherwise whoever has it keeps it. So a page covered by a root push still flies back on pop.
Flights still work as in Flutter:
- inside a nested navigator, on push, pop and back swipe;
- between a nested page and a root page, both ways;
- from the visible tab after a tab switch.
Covered by your repro, a go_router-style Offstage/TickerMode branch, a tab with a page pushed inside it, and a StreamScaffold around the tabs.
A page transition collects heroes from its own page and from the current page of every navigator nested in it, so tabs with their own navigators each carried the snackbar hero and a root push hit the duplicate-tag assert. Surfaces collected by the same transition now never carry it at once; a visible one takes it from a hidden one, and otherwise the carrier keeps it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>


What
StreamSnackbarScopenow shows in eachStreamScaffoldunder it, resting above itsbottominstead of covering it. A scaffold nested in another under the same scope leaves it to the outer one. While the scope has noStreamScaffoldunder it, the snackbar shows at the scope's own bottom edge, as before.StreamScaffoldshows its snackbars on theStreamScaffoldbeneath it: behind a dialog's barrier, or hidden under an opaque page. WithresizeToAvoidBottomInset: false, the snackbar sits behind the keyboard. Both match Flutter'sScaffold.StreamScaffoldover another flies the snackbar across as one copy, like Flutter'sSnackBarhero. Which copy carries the hero follows how Flutter collects heroes, including nested navigators (tabs, go_router shells): the visible copy flies, and two never carry it at once, so Hero's duplicate-tag assert can't fire where Flutter's ownScaffoldwould.Scaffolddoes.StreamSnackbarPopupis unchanged.Why
The app-wide scope draws at the bottom of the screen and has no idea what the page docks there. In chat, snackbars from the message list (copy, pin, delete…) covered the composer, and on the channel list they covered the bottom nav bar.
This follows Flutter's
ScaffoldMessengermodel: one queue at the scope, every top-level scaffold shows it, a nested scaffold leaves it to the outer one. Because the queue stays at the scope, a snackbar shown just before a page pops still appears on the page below. Like Flutter, the snackbar snaps (rather than animates) to its new position when the bottom chrome changes.Before / after
Pinning a message in the stream-chat-flutter sample (Pixel 10 emulator), against the currently pinned core vs this branch:
StreamScaffoldVerified
stream_scaffold_snackbar_placement_test.dartstream_snackbar_scope_surface_test.dartstream_snackbar_scope_messenger_test.dartflutter test --exclude-tags golden: 526 passing; the scaffold and snackbar suites pass on Flutter 3.44.0 and 3.47.7; analyze, format:verify and check:barrels clean.StreamScaffold, and the off-screen error. Earlier on 0ec5f27, where the scope and snackbar slot are unchanged: aStreamScaffoldin a dialog, Undo/swipe, the loading spinner, one TalkBack node, and the composer'sStreamSnackbarPopupunchanged. Tab/D-pad focus is unit-tested only.🤖 Generated with Claude Code
Summary by CodeRabbit
StreamSnackbarScopenow appear within the top-levelStreamScaffold, above its bottom content. Pages without their own scaffold display the snackbar on the scaffold beneath them.StreamScaffoldgallery, with regular and floating bottom-bar styles.