Tabs & adaptive

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.