back_stack 0.6.1 copy "back_stack: ^0.6.1" to clipboard
back_stack: ^0.6.1 copied to clipboard

You own the back stack. Navigation is a list you push and pop — type-safe, observable, no route graph.

0.6.1 #

A correctness pass over the async-gating, deep-link, and page-memo edges. Still zero dependencies.

  • AsyncRedirect.invalidate() now cancels in-flight checks. Previously it only cleared resolved cache entries: a check already awaiting (an auth or session lookup) could still complete and apply a decision made from the very state invalidate() declared stale — e.g. redirecting a now-signed-in user to Login. Each check is pinned to a cache generation; a check that resolves after an invalidate() is dropped (not cached, not applied) and any held navigation is released, so the next navigation re-checks against fresh state.
  • A deep-link source that errors no longer surfaces as an unhandled async error. BackStackApp/BackStackTabsApp hardened URI decoding, but an error from initialLink or linkStream itself went uncaught. Both are now caught and dropped — the app stays put and the stream keeps delivering later links — consistent with the existing "bad links don't crash" behavior.
  • Changing the decorators list now invalidates the memoized pages. NavDisplay re-memoized pages on a stack/builder/entries/pageBuilder change but not on a decorators change, so with a stable builder existing pages kept their old decorated subtree. The decorator list now participates in memo invalidation (compared element-wise, so stable decorators keep the memo).
  • Raised the Flutter lower bound to >=3.24.0 to match the API surface actually used (Navigator.onDidRemovePage, PopScope.onPopInvokedWithResult); the old >=1.17.0 was far too low.

(The redirect-safe pushForResult path reported alongside these was already fixed in 0.5.0; a regression test now locks it in.)

0.6.0 #

iOS swipe-back on TransitionPage #

  • The edge-swipe back gesture now works on pages with custom transitions. TransitionPage used to build a plain PageRoute, so on iOS the standard drag-from-the-left-edge gesture did nothing — a screen pushed with .fade or a shared-axis transition could only go back via a button. On iOS (defaultTargetPlatform), dragging from the leading edge now drives the page's own transitionsBuilder in reverse and pops past the halfway point or on a fling, exactly like CupertinoPageRoute — and the pop syncs the NavStack as usual. RTL layouts swipe from the right edge.
  • New swipeBack parameter on every TransitionPage constructor to opt a page out (defaults to true; TransitionPage.none defaults to false). The gesture also stays off for fullscreenDialog pages, non-opaque pages, the root of the stack, and any screen whose PopScope/ConfirmPopScope blocks the pop — same rules as the framework's popGestureEnabled. (Like the AppBar back button, it goes through the Navigator, so the programmatic-pop veto stack.popGuard doesn't apply.)
  • No behavior change on Android or desktop, and zero dependencies as always.

0.5.0 #

A correctness release for everything back-button-adjacent, driven by an adversarial audit of 0.4.0. Still zero dependencies.

System back now routes innermost-first, everywhere #

  • Tabs: OS back reaches the active tab's Navigator first. MultiNavStackRouterDelegate.popRoute() (and MultiNavDisplay's own PopScope) used to pop the active tab's stack model directly, skipping the tab's routes entirely — back would pop the page under an open dialog, or leave the app with a dialog still up, and a screen's PopScope/ConfirmPopScope inside a tab never saw the gesture. Back now goes through the active tab's navigator via maybePop (dialog → screen's PopScope → tab page → first-tab fallback → app close), tracked live via NavigationNotification so an open dialog is known even at a tab root. The delegate exposes its per-tab navigatorKeys, and MultiNavDisplay accepts navigatorKeys: for custom Router setups.
  • NavDisplay(nested: true) routes back through its child navigator the same way, instead of popping the stack directly — dialogs, sheets and PopScopes inside a nested display now get first refusal.

AsyncRedirect: no flash of protected content, fail closed #

  • New gates: predicate — declare what's protected. Navigation matching gates is now held off screen while check runs: the stack stays where it is (as the docs always claimed), resolving drives your scrim, and the held navigation is applied — or bounced — when the check resolves. Everything not matching gates navigates instantly and is never checked, so pops and pushForResult results are unaffected. Without gates the old show-then-correct behavior remains (holding everything would swallow ordinary results); pass gates to get the hold.
  • A throwing check now fails closed: the navigation is dropped, the user stays put, and the error is reported via FlutterError.reportError (it used to be silently allowed — an auth gate that failed open).
  • A pushForResult into an undecided gated destination resolves null immediately; un-gated destinations are unaffected. Documented.

Browser back/forward restores the full typed stack #

  • Each history entry now carries a full-stack snapshot (RouteInformation.state), so a browser back/forward hop restores the entire typed stack — in-app depth the URL can't express included — instead of rebuilding from the URL's projection. Automatic in BackStackApp/BackStackTabsApp when links (or restoreWith) is set; custom Router setups opt in via the delegates' new stateCodec. A missing, corrupt, or URL-disagreeing snapshot falls back to the URL mapping, exactly like a deep link. The tabs snapshot covers every tab's stack plus the active tab.
  • Breaking (type-level only): the delegates' Router configuration type changed from Uri to NavRouteInformation (uri + state), and NavStackRouteInformationParser now parses that type. Apps using BackStackApp/BackStackTabsApp are unaffected; direct setNewRoutePath(uri) callers wrap the uri: NavRouteInformation(uri). The imperative link API (handleLink/handleLinkAsync) still takes a plain Uri.

Fixes #

  • Tabs observers could never work — replaced with observersFor. A NavigatorObserver belongs to exactly one Navigator (the framework asserts it), but the tabs widgets attached the same list to every tab's navigator — any non-empty observers with 2+ tabs crashed in debug on the first frame. MultiNavDisplay/MultiNavStackRouterDelegate/ BackStackTabsApp now take observersFor: (tabIndex) => [...], called once per tab; observers remains for single-tab hosts and is rejected with a clear message otherwise.
  • Core: a redirect no longer breaks pushForResult. _commit reconciled the redirect's result against the pre-change stack, so any attached redirect — even one that allowed the navigation unchanged — rebuilt the just-pushed entry with a fresh identity and its pushForResult awaiter resolved null. The redirect result now reconciles against the proposed entries, keeping the pushed entry's id (and awaiter) when it's allowed through.
  • BackStackApp with no links/onLink: a runtime deep link now keeps the app where it is (as documented) instead of resetting navigation to its launch state.
  • Memoized pages no longer freeze inherited reads. A screen built by builder/entries now rebuilds when a Theme / localization / media-query dependency changes — previously a live entry's screen was built exactly once and silently never updated.

Docs #

  • README: clarified that restoration restores the stack (destinations, in order, typed); state inside a screen follows Flutter's own restoration APIs.
  • NavStackCodec: documented that browser back/forward rebuilds the stack from the URL via decode, like a deep link — in-app depth beyond the URL's projection isn't preserved across a history hop.

0.4.0 #

The "handled internally" release: the app shapes and URL plumbing every real app needs, each reduced to one parameter — plus a hardening pass over the core. Still zero dependencies.

One source of truth for URLs #

  • NavLinks — the URL table. One entry per linkable destination declares both directions at once ('/products/:id'Product), with typed parameter reads (NavMatch), query parameters, *rest catch-alls, a per-destination parents: (the layer-vs-replace choice), and notFound. Deep links, the web address bar, browser back/forward, shareable links (linkFor) and state restoration all derive from the same table, so the two directions can never drift apart. It's a NavStackCodec, so it plugs in anywhere one goes — usually BackStackApp(links: …). It never owns the stack; back_stack still has no route graph.
  • Round-trip drift validator. In debug builds the router verifies encode∘decode is idempotent per URL and reports a drifted codec the moment it's introduced — the classic hand-written onLink/toLink bug, caught at development time.

One-widget apps #

  • BackStackApp is now a true one-widget app: onLink is optional (BackStackApp(stack: …, entries: …) is a complete app), it accepts links: and entries: directly, and it forwards the previously missing MaterialApp knobs (onGenerateTitle, scaffoldMessengerKey, locale callbacks, high-contrast themes, shortcuts/actions, appBuilder). A shell: parameter wraps persistent chrome (side rail, overlays) around the navigating area, under MaterialApp. Config (onLink/toLink/builder) is read live, so hot reload and rebuilds take effect without losing navigation state.
  • BackStackTabsApp — the tabbed app, bundled. Bottom navigation with a persistent back stack per tab used to take up to nine hand-wired objects; it's now one widget: pass tabs: + destinations: (built-in NavigationBar, selection and re-tap-to-pop-to-root wired) or shell: (your own chrome). Works with links: — the target tab is inferred from the decoded stack's root — plus onLinkAsync, initialLink/linkStream, lazy tabs, and automatic restoration of every tab's stack + the active tab.

Async everywhere #

  • Async link resolutiononLinkAsync on both apps (and asyncDecode on both router delegates): await a lookup before deciding what a link shows. Race-safe by design: a newer link supersedes an in-flight one, and errors fall through to the sync mapping.
  • Async popNavStack.popGuardAsync + tryPop(): an "are you sure?" dialog or save-before-leave call for programmatic pops, race-safe (if the stack changes while the guard is deciding, nothing is popped).
  • AsyncRedirect.attach(stack) wires redirect + refreshListenable in one call — forgetting the second half used to make gating silently hang. detach() undoes it; dispose() auto-detaches. Also hardened: disposing mid-check no longer crashes, and the decision cache is bounded (cacheSize) and keyed by real element-wise equality instead of a raw hash.

Full-stack restoration, automatic #

  • With links: set, the entire typed stack (and for tabs: every tab's stack + the active tab) survives process death — serialized through the same URL table, unregistered keys skipped, corrupt snapshots discarded. restoreWith: accepts a NavKeyCodec for destinations without URLs. A cold-start deep link still wins over the snapshot unless the snapshot is just the deeper history of the same location. (RestorableBackStack / RestorableMultiNavStack remain for Router-free setups.)

Per-destination presentation #

  • NavEntries.on<T>(page: …) — a destination can carry its own dialog, sheet, or transition, with decorators still applied: no global pageBuilder switch and no mixin on domain keys. Displays and delegates accept the registry directly (entries: as the alternative to builder:).

Typed navigation events, ergonomics & sugar (from the previous batch) #

  • NavStackObserver<K> — typed onScreen/onPush/onPop in your keys.
  • NavPath — typed URL reading/building sugar for hand-written codecs.
  • Ergonomic stack ops: pushAll, popToRoot, removeWhere.
  • BackStackApp.initialLink — cold-start link, awaited and applied through the same mapping.
  • pushOrMoveToTop now returns whether the stack changed — making it the double-tap-proof push (a second tap is a no-op, not a duplicate screen).
  • replaceTop(key, result: …) completes the replaced screen's pushForResult awaiter with a value.
  • pushForResult's result type is checked at the pop(result) call site in debug, instead of failing later as a cast error inside the await.

Fixes & hardening #

  • NavDisplay page memoization survives parent rebuilds with the documented entries.call tear-off (function equality instead of identity), and onRemoved still fires after a builder swap.
  • popToRoot is one commit and respects popGuard on the current top; tab re-select uses the same semantics (one notification, protected forms stay).
  • A deep link targeting an out-of-range tab now falls back instead of being silently dropped.
  • Applying a platform route identical to the current location is a no-op (protects restored deep stacks from being collapsed to their URL projection).
  • NavLinks/link decoding never throws — including trailing slashes and URLs whose percent-escapes aren't even valid UTF-8.
  • Shell chrome gets its own Overlay (tooltips/menus in a bottom bar work).
  • Shared-axis and fade-through transitions animate at the render-object level (no per-frame widget rebuilds).
  • BackStack.of/MultiBackStack.of/parentOf throw real FlutterErrors in all build modes, with a "did you forget the type argument?" hint; maybeParentOf stops walking once found; custom pages that drop the supplied pageKey are caught by a debug assert (they would silently desync system back).
  • The package archive no longer ships the 2 MB demo gif (3 MB → ~0.1 MB).

0.3.0 #

A feature drop aimed at everyday UX — all within the same few-concepts, zero-dependency model (nothing here changes how you own the stack).

  • AsyncRedirect — loop-proof async auth gating. redirect stays synchronous (that's what makes it loop-proof), so async gates that need to await — a permission call, a session refresh, a "does this deep-linked doc exist" check — now have a first-class home. Assign gate.call to redirect and the gate to refreshListenable: navigating to a gated destination holds it while the check runs (gate.resolving drives a loading overlay), then either allows it or redirects. Decisions are cached per destination so a check runs once; call gate.invalidate() after login/logout. Built entirely on the existing sync primitives — the core is unchanged.
  • Material motion transitions on TransitionPage. Adds sharedAxisHorizontal / sharedAxisVertical / sharedAxisScaled (X/Y/Z) and fadeThrough alongside the existing fade/slideUp/scale. Hand-rolled, dependency-free, and the shared-axis / fade-through motions animate the outgoing screen too. Pick the motion by the relationship between screens (peer step, into-a-hierarchy, unrelated switch).
  • Migration guide from go_router (doc/MIGRATING_FROM_GO_ROUTER.md) — a mapping table plus before/after for routes, navigation calls, typed arguments, redirects (incl. async), shell/tab routes, deep links and error handling, with notes on migrating gradually.
  • Examples: example/lib/guarded.dart (async gating with a loading overlay) and example/lib/motion.dart (per-destination Material motion).

0.2.8 #

  • Async deep links from native. The platform's Router only surfaces the launch URL and standard app links it resolves itself. Links that arrive while the app runs — a custom scheme (myapp://…), a Firebase Dynamic Link, a warm app_links link — come from a native plugin as a Stream<Uri>. BackStackApp now takes an optional linkStream: hand it that stream and every emission runs through the same onLink, with the same never-throws fallback as a platform link. back_stack owns the subscription lifecycle and cancels it on dispose; you bring the Uris from whatever plugin you use — no new dependency.
  • NavStackRouterDelegate.handleLink(uri) / MultiNavStackRouterDelegate.handleLink(uri) — the imperative sibling of setNewRoutePath, for driving your own router: apply a runtime link to the stack with the identical decode-or-fallback hardening. (setNewRoutePath now delegates to it, so platform and runtime links share one code path.)

0.2.7 #

A correctness + documentation pass. No API changes — every fix is behavior that matches what the docs already promised.

  • Fix — NavListDetail no longer crashes when a lone detail is on top on a wide screen. Previously, if the only (or first) entry was itself a detail type, it was mounted in both panes under the same per-entry GlobalKey and threw a duplicate-key error. It now shows as the list pane with an empty detail pane. Regression-tested.
  • Fix — NavEntryDecorator.onRemoved is now symmetric across the breakpoint in NavListDetail. onRemoved fires only for entries that were actually rendered (as a wide pane or a narrow page), so a wide-only middle entry that was never shown no longer gets a spurious onRemoved with no matching decorate.
  • Fix — MultiNavStack.handleBack consumes back even when a popGuard vetoes the pop. With history in the active tab, back is now treated as handled (so the app stays open) instead of surfacing the vetoed pop's false to the host PopScope — which would have closed the app.
  • Hardening — an empty stack is refused rather than crashing in release. A redirect/replaceAll that resolves to no destinations is caught by an assert in debug and, in release, declined (the current stack stays) instead of leaving the Navigator with an empty pages list.
  • Examples: adds example/lib/tabs.dart (MultiNavStack bottom nav with per-tab history) and example/lib/results.dart (pushForResult — await a value from a pushed screen), filling the two biggest example gaps.
  • Docs: README gains a table of contents and runnable snippets for results, per-tab nav, reusing an open screen (pushOrMoveToTop/moveToTop) and the live BackStackInspector; corrects the BackStack.of<AppKey> type argument, the exhaustiveness claim (now that the hero leads with NavEntries), and the example README's Pokédex description. pushForResult's Object?-cast caveat and the NavEntry.id / entries docs are clarified.

0.2.6 #

Convenience additions inspired by other routers — all built as plain list ops / pure functions / one widget, so the core model is unchanged (no codegen, no dependencies).

  • NavStack.moveToTop(test) / pushOrMoveToTop(key) — reuse a screen that's already open instead of stacking a duplicate (singleTop/clearTop).
  • combineRedirects([...]) — compose several redirect rules (auth, onboarding, flags) into one redirect, each a pure proposed → RedirectStep (ContinueRedirect / RedirectTo(stack, stop:)). Same once-per-change, loop-proof semantics as a single redirect.
  • BackStackInspector<K> — a zero-dep in-app widget that lists the live stack (current entry marked), for debugging without any DevTools plumbing.

Not added, on purpose: a string query-parameter bag (typed key fields already carry a destination's arguments — a stringly-typed side channel would work against that) and a route-file code generator (back_stack stays codegen-free).

0.2.5 #

  • BackStackApp<K> — deep links in one function. A single widget bundles the MaterialApp.router + NavStackRouterDelegate + parser wiring; you supply only onLink: (Uri) => [destinations]. Optional toLink projects the stack back onto the web address bar, onLinkFallback handles bad links, and restorationScopeId is set by default. Zero new dependencies — it drives Flutter's own Router.
  • BackStack.parentOf<K>(context) (and maybeParentOf) — reach the stack one level up from a nested child when parent and child share the same key type, so a child screen can BackStack.parentOf<AppKey>(context).pop() the outer flow. (Distinct key subtypes + BackStack.of<ParentKey> remain the more type-safe default.)
  • example/lib/multi_file/ now uses BackStackApp — the simplest example also demonstrates deep links (/product/7).

0.2.4 #

  • Docs: the README hero snippet is corrected and no longer uses a switch — it leads with NavEntries (..on<Home>(...), builder: entries.call) over a non-sealed AppKey, so it compiles as shown and mirrors a multi-file app. (The previous snippet declared a sealed base with no const constructor and inferred NavStack<Home>, so it didn't compile.)
  • Adds example/lib/multi_file/ — the simplest multi-file layout: app_key.dart
    • one file per feature (home_feature.dart, product_feature.dart) each with its own destination, screen and register* call, collected in main.dart. No switch, no central list.

0.2.3 #

  • Fix: a pop(result) that is vetoed by guard (or collapsed to a no-op by redirect) no longer completes the destination's pushForResult future or returns true. Previously the awaiter received the result while the screen was still on top — now the result is delivered, and pop returns true, only when the entry actually leaves the stack. (popGuard was already safe.)
  • Docs: NavEntryDecorator.onRemoved notes that it runs during the display's build (defer any rebuild-triggering work with addPostFrameCallback); NavEntries documents that it matches by exact runtime type, not subtype.
  • Adds example/lib/modular_demo.dart (also on GitHub): three feature modules register their own NavEntries into one map, and a single NavEntryDecorator wraps every screen and logs teardown on onRemoved.

0.2.2 #

  • The main example (example/lib/main.dart, the one shown on the package page) now demonstrates NavEntries (as the builder) and NavEntryDecorator (a screen_view log + teardown hook) — so both APIs are visible on pub.dev's Example tab. No library code changed.

0.2.1 #

  • Add NavEntries<K> — a registrable destination-type → screen map (..on<Home>(...)), Compose Nav3's entryProvider. Pass builder: entries.call to NavDisplay. Composes across feature files/modules instead of one big switch. The switch stays the default (compile-time exhaustive); this is the modular option.
  • Add NavEntryDecorator<K> — Nav3's NavEntryDecorator. decorate wraps every screen (providers/DI scopes/tracing; first decorator is outermost) and onRemoved fires when an entry leaves the stack (or the display is disposed) so you can tear down a Bloc/controller/scope tied to a destination. Wire via the new decorators: on NavDisplay — also forwarded by MultiNavDisplay, NavSceneHost, NavListDetail, NavStackRouterDelegate and MultiNavStackRouterDelegate.

0.2.0 #

  • Add NavStackCodec.of(encode:, decode:, fallback:) — build a deep-link codec inline from two functions instead of subclassing NavStackCodec.
  • Add NavStackCodec.fallbackFor(uri) — the stack shown when a link is malformed or unknown (defaults to decoding /; supply fallback: or override to route to a NotFound screen).
  • Harden the deep-link boundary: NavStackRouterDelegate now decodes without ever throwing — a decode that throws or returns empty falls back instead of crashing the app, so codecs can parse optimistically.
  • Fix: pushForResult no longer hangs (and leaks its awaiter) when the push is blocked by guard or collapsed to a no-op by redirect — the future now resolves null, like every other way a screen can leave the stack.
  • Web + tabs: MultiNavStackRouterDelegate + MultiNavStackCodec bring URL sync, deep links and OS back to a MultiNavStack (per-tab history) — the multi-tab equivalents of NavStackRouterDelegate / NavStackCodec.
  • Restoration + tabs: RestorableMultiNavStack persists every tab's stack and the active tab across process death. Both restorable widgets now survive a corrupt/incompatible snapshot by keeping the freshly created stack instead of crashing on cold start.
  • MultiNavDisplay gains lazy (build a tab only once first selected; default false, unchanged behavior) and observers. observers is also now forwarded by NavSceneHost, NavListDetail and NavStackRouterDelegate — a clean NavigatorObserver seam for screen_view analytics.
  • NavListDetail now preserves pane State across the breakpoint. Rotating or resizing between the two-pane and stacked layouts keeps each screen's scroll position and controllers — every entry gets a stable GlobalKey, so Flutter reparents the live screen instead of rebuilding it.
  • Add MultiBackStack.of(context) (and MultiNavStackScope) — reach the MultiNavStack host from any tab screen to switch tabs / read the active index, without passing the host down by hand.

0.1.1 #

  • Docs: the demo GIF now uses an absolute URL so it renders on the pub.dev package page (a relative path inside a raw <img> tag wasn't rewritten).

0.1.0 #

  • Fix: Hero (shared-element) transitions now animate inside a nested NavDisplay — e.g. under NavListDetail or MaterialApp(home:). Each display drives its own HeroController via a HeroControllerScope, so a sprite flies between screens instead of snapping. Previously only the root/Router display inherited one. Regression-tested.

0.0.1 #

  • Adaptive layout: NavListDetail (one stack → two-pane on wide, animated stack on narrow) and the general NavSceneHost + NavSceneStrategy engine (listDetailScene, supportingPaneScene) — Compose Nav3 "scenes" over one list.
  • Multi-stack: MultiNavStack + MultiNavDisplay give a bottom-nav persistent per-tab back stack (IndexedStack), with innermost-first system back.
  • Custom pages: TransitionPage (.fade/.slideUp/.scale/.none), DialogPage, SheetPage, and a per-key NavPage mixin so a destination declares its own transition.
  • ConfirmPopScope: async confirm-before-leave covering the Android system back.
  • RestorableBackStack + NavKeyCodec: full-stack restoration across process death without a URL.
  • NavDisplay.nested flag routes the system back gesture into a nested stack.
  • Minimal rebuilds: pages are memoized by entry id, so one push rebuilds exactly one screen regardless of stack depth (see benchmark/).
  • popGuard (sync pop veto) and refreshListenable (re-run redirect on auth change) on NavStack.
  • NavStack: the back stack you own. push / pop / replaceTop / replaceAll / popUntil / edit. BackStack.of(context) reaches it from a screen (auto-provided by NavDisplay).
  • Auth gating: redirect (pure transform, applied once — loop-proof) and guard (veto).
  • NavDisplay: renders the stack via the Pages API; system / predictive / hardware back sync into the list automatically (onDidRemovePage).
  • NavKey: type-safe destinations as plain Dart objects. EquatableNavKey mixin gives them value equality so a URL re-decode reuses the live screen.
  • Identity & State preservation: stable unique id per entry (unique page key); duplicate destinations are independent; replaceAll/edit reconcile so survivors keep their State and only changed screens rebuild.
  • Result passing: pushForResult<T>() -> Future<T?>, completed by pop([result]) or null on any other removal / dispose (never hangs/leaks).
  • Router integration: NavStackRouterDelegate + NavStackRouteInformationParser
    • a NavStackCodec (Uri <-> List<NavKey>): web URL sync, deep links (you choose layer-vs-replace), browser/OS/predictive back, and state restoration.
  • Leak-safe: leaving the stack disposes the route (proven by test); suite runs under leak_tracker_flutter_testing. 42 tests + benchmarks, analyze clean under very_good_analysis (strict casts/inference/raw-types).
  • Docs: doc/PHILOSOPHY.md maps Flutter's model + the nav leak/caveat catalog.
5
likes
160
points
1.06k
downloads

Documentation

API reference

Publisher

verified publisherandroidpoet.dev

Weekly Downloads

You own the back stack. Navigation is a list you push and pop — type-safe, observable, no route graph.

Repository (GitHub)
View/report issues

Topics

#navigation #router #navigator #routing

License

MIT (license)

Dependencies

flutter

More

Packages that depend on back_stack