orbital_router 0.5.0
orbital_router: ^0.5.0 copied to clipboard
Flutter router integration for Orbital modules, scopes and deep links.
orbital_router #
Flutter routing integration for Orbital modules, scopes, deep links and navigation guards.
Imports #
orbital_router exports the routing API plus the core contracts used by route
declarations.
If you want the default scope runtime, import orbital_injector as well:
import 'package:orbital_injector/orbital_injector.dart';
import 'package:orbital_router/orbital_router.dart';
If you prefer a single import surface, use package:orbital/orbital.dart.
updateRoutingConfig swaps are serialized: a swap issued while a previous one
is still applying waits for it to finish, so two rapid swaps can never release
the same stack scopes concurrently. Swaps are rejected with
OrbitalShellRoutingConfigChangeException while a shell route is mounted —
navigate away from the shell before swapping the routing config.
What this package contains #
orbital_router connects the Orbital scope model to Flutter navigation:
OrbitalRouter(...)OrbitalRouter.routingConfig(...)OrbitalModule,OrbitalPageRouteandOrbitalModuleRouteOrbitalRouterDelegateOrbitalRouteContextOrbitalMiddlewareandOrbitalAsyncMiddlewareOrbitalRetentionPolicyOrbitalReactivationPolicyOrbitalScopeFactoryOrbitalScopeProviderplusBuildContextextensions- router debug snapshots consumed by
orbital_devtools
Quick start #
final router = OrbitalRouter(
scopeFactory: const DefaultOrbitalScopeFactory(),
initialUri: Uri.parse('/'),
modules: [
OrbitalModule(
path: '/',
routes: [
OrbitalPageRoute(
path: '/home',
builder: (context, routeContext) => const HomePage(),
),
],
),
],
);
MaterialApp.router(
routerConfig: router,
);
Keep the OrbitalRouter instance stable for the lifetime of the owning app
state, and call router.dispose() from State.dispose() when you create it
manually.
Pass onNavigationError to observe navigation failures that happen
mid-session (after the router has already committed to a route) without
disrupting the currently visible page:
OrbitalRouter(
// ...
onNavigationError: (error) => scaffoldMessengerKey.currentState
?.showSnackBar(SnackBar(content: Text('Navigation failed: $error'))),
);
The DevTools bridge auto-enables in debug mode as soon as OrbitalRouter is
created, so the orbital_devtools panel works with zero application code —
add orbital_devtools to dev_dependencies and you're done. Pass
enableDevtoolsBridge: false to opt out, or devtoolsAppId to customize the
app id shown in the panel:
OrbitalRouter(
// ...
devtoolsAppId: 'my_app', // or: enableDevtoolsBridge: false
);
Route tree model #
Orbital composes navigation from modules and page routes.
Modules contribute:
- a base path
- module-scoped bindings
- module-scoped middlewares
- nested pages or submodules
- optional
onInit/onExitlifecycle callbacks
onInit(resolver) runs once, right after the module's own bindings finish
initializing; onExit(resolver) runs once, right before the module's scope
is actually disposed. Neither reruns when a persistentScope module is
retained and later reactivated — only on a genuine create/dispose:
OrbitalModule(
path: '/chat',
bindings: [
OrbitalBinding.singleton<ChatSocketService>((resolver) => ChatSocketService()),
],
onInit: (resolver) => resolver.get<ChatSocketService>().connect(),
onExit: (resolver) => resolver.get<ChatSocketService>().disconnect(),
routes: [...],
)
Pages contribute:
- a page path
- page-scoped bindings
- page-scoped middlewares
- a widget builder
- optional retention semantics
- optional route and async page presentation overrides
Matching and module identity #
Route matching prefers more specific branches first. Static segments outrank parameterized segments; declaration order is only the final tiebreaker.
OrbitalModule.id is the semantic identity used for scope reuse across rebuilt
declarations. If you want a module scope to survive equivalent rebuilds, supply
an explicit id.
Without id, reuse falls back to the module declaration identity inside the
current delegate instance, which prevents unrelated modules that merely share
the same path from accidentally reusing each other's scope.
Async navigation #
A branch is treated as async when it contains async bindings or async middlewares.
Use:
asyncNavigationBuilderasyncNavigationPageFactorypageFactory
Precedence is:
- page
- nearest module
- app-level default
Custom pageFactory and asyncNavigationPageFactory implementations must
return a Page that preserves the supplied key. Orbital relies on that key
to keep Navigator.pages identity stable across pops, redirects and rebuilds.
During async navigation, Orbital keeps the currently rendered route mounted under the async page by default. The fallback async page is non-opaque, so the active page remains visible unless a custom async page factory chooses a different presentation.
Experimental shell routes #
OrbitalShellRoute mounts independent branch navigators inside a single shell
page (bottom navigation, tabs, etc.). Each branch owns its own stack while the
shell keeps inactive branches alive in an IndexedStack.
OrbitalShellRoute(
path: '/app',
initialBranchIndex: 0,
shellBuilder: (context, shell) => Scaffold(
body: OrbitalShellBranchStack(controller: shell),
bottomNavigationBar: NavigationBar(
selectedIndex: shell.activeIndex,
onDestinationSelected: shell.switchBranch,
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'),
],
),
),
branches: [
OrbitalModule(path: '/home', routes: [...]),
OrbitalModule(path: '/settings', routes: [...]),
],
)
Cross-branch pushNamed calls switch branches automatically. Deep links that
match a branch URI route into the shell instead of the root not-found page.
Authenticated branch scopes stay alive until shell.disposeBranches() runs or
the shell unmounts. A logout flow that only calls pushNamedAndRemoveAll('/login')
leaves those scopes (and any sessions or tokens they hold) alive in memory. Call
disposeBranches() as part of logout before navigating away, and only recreate
the branches on the next authenticated session:
await shell.disposeBranches();
await router.routerDelegate.pushNamedAndRemoveAll('/login');
Shell lifetime vs host-stack resets #
A shell stays alive while host-level routes are pushed above it — the shell is
only deactivated (its branches keep running) until it becomes the top again.
It is disposed only when the shell route itself leaves the host stack.
A host-level pushNamedAndRemoveAll(...) clears every stack entry,
including the shell, so the next visit re-creates the shell and its branch
controllers from scratch (branch onInit re-runs and per-page state is lost).
Marking the shell module and route persistentScope: true retains the scope
tree, but branch controllers are still re-created because they are widget
state. To keep the shell (and the branch below it) alive while navigating to a
host route, prefer pushReplacementNamed(...) or a plain pop over
pushNamedAndRemoveAll(...).
OrbitalAsyncNavigationState.background remains available for custom builders
that want to compose an explicit background widget.
Middleware #
OrbitalMiddleware can:
- continue navigation
- block navigation
- redirect to another URI
Hook points:
onBeforeRunonAfterRun
Middlewares that may await must implement OrbitalAsyncMiddleware.
Guards currently run after Orbital has resolved the destination module and page scopes. This keeps guard code able to read scoped dependencies, but it also means a blocked navigation may pay the initialization cost for the attempted destination. Keep heavy or side-effecting initialization out of guarded bindings when unauthenticated users commonly hit those routes.
Scope access and navigation helpers #
Pages can read route and scope state from:
OrbitalRouteContextcontext.orbitalRouteContextcontext.orbitalScopecontext.getOrbital<T>()context.pushNamed('/path')context.pushReplacementNamed('/path')context.pushNamedAndRemoveAll('/path')
Orbital now renders a real Navigator.pages stack.
pushNamed/pushUripush a new page entrypushReplacementNamed/pushReplacementUrireplace only the top entrypushNamedAndRemoveAll/pushUriAndRemoveAllreset the stack to a single destinationpop()/popRoute()/popUntil(...)remove stack entries and reactivate the previous one
By default, reactivation preserves page and module scopes exactly as they were.
OrbitalReactivationPolicy can opt routes or modules into rerunning
middlewares when a stacked entry becomes active again. Precedence is router,
then outer-to-inner modules, then the page route.
When OrbitalRouter.routingConfig(...) changes but all live entries still match
the new route tree, Orbital preserves the current stack instead of collapsing it
to a single page. If the active URI disappears from the new config, Orbital
shows the configured not-found page for that URI and clears invalid stack
entries.
On the web, browser back/forward route information is currently treated as a
replace-all platform route update. Orbital does not yet restore the logical page
stack from RouteInformation.state, so browser history should be considered
URL-level navigation rather than full in-memory stack restoration.
Retention and scope rules #
- page bindings require
createScope: true - persistent module and page scopes can be retained across navigations
- page retention keys default to
path + query + fragment persistentScopeKeyBuilderlets apps override page retention semanticsOrbitalRetentionPolicybounds retained module/page entries and evicts the oldest retained scope when capacity is exceeded
More detail #
See doc.md for lifecycle flow, stack reactivation, middleware orchestration, async overlay behavior and error semantics.