Tabs & adaptive layouts
Tabs — one widget
BackStackTabsApp is the tabbed app, bundled: bottom navigation with a
persistent back stack per tab, system back handled innermost-first, re-tapping
the active tab pops it to root.
void main() => runApp(
BackStackTabsApp<AppKey>(
tabs: [
NavStack<AppKey>.of(const Feed()),
NavStack<AppKey>.of(const Search()),
NavStack<AppKey>.of(const Profile()),
],
entries: entries,
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Feed'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
links: links, // optional
),
);Push deep in one tab, switch away and back — the history is intact. With
links: set, a deep link lands in the right tab automatically (the tab is
inferred from the decoded stack's root destination, matched against each tab's
initial root), and every tab's stack plus the active tab survive process
death — see Restoration. onLinkAsync,
initialLink/linkStream, lazy: tabs, initialIndex: and
popToRootOnReselect: all work the same as on BackStackApp.
Your own chrome
Want a rail, a drawer, or a custom bar? Pass shell: instead of
destinations: — it receives the MultiNavStack host:
BackStackTabsApp<AppKey>(
tabs: [...],
entries: entries,
shell: (context, host, child) => Scaffold(
body: Row(children: [MyRail(host: host), Expanded(child: child)]),
),
);Drive selection from host.index / host.select(i), and reach the host from
any screen with MultiBackStack.of<AppKey>(context) — e.g. to switch tabs from
deep inside one.
List-detail from one stack
NavListDetail renders a single stack as a two-pane list-detail on a wide
window and a push/pop stack on a phone — no second navigation model:
NavListDetail<AppKey>(
stack: stack,
isDetail: (key) => key is Message,
list: (context, key) => const Inbox(),
detail: (context, key) => MessageView(id: (key as Message).id),
placeholder: (context) => const Text('Pick a message'),
);Each pane keeps its State across the breakpoint — every entry gets a stable
GlobalKey, so screens are reparented, not rebuilt. For a supporting pane, three
columns, or your own layout, drop to the general NavSceneHost + NavSceneStrategy
engine over the same one stack.
Full control
BackStackTabsApp is built from public pieces you can wire yourself:
final host = MultiNavStack<AppKey>([
NavStack.of(const Feed()),
NavStack.of(const Search()),
]);
Scaffold(
body: MultiNavDisplay<AppKey>(host: host, entries: entries),
bottomNavigationBar: ListenableBuilder(
listenable: host,
builder: (context, _) => NavigationBar(
selectedIndex: host.index,
onDestinationSelected: host.select, // re-tap active tab → pop to root
destinations: const [/* ... */],
),
),
);For URL sync, deep links and browser back at this level, drive it from the
Router with MultiNavStackRouterDelegate + a MultiNavStackCodec
(Uri ⇄ (tab, stack)); to survive process death, wrap it in
RestorableMultiNavStack. On BackStackTabsApp the same full control is the
onLink:/toLink: pair over MultiNavLocation — which tab plus the stack to
show in it.