Navigation & Routing Architecture
Neon Framework features a built-in, standalone navigation and routing subsystem implemented in 100% pure Dart with zero Flutter dependencies.
Designed specifically for Neon's decoupled architecture, the router seamlessly connects the Dart engine with native Android (Kotlin / Jetpack) and iOS (Swift / UIKit) shells over the JSON bridge.
1. Neon vs. Flutter Navigator
Flutter's navigation systems (Navigator 1.0, Navigator 2.0 / Router Delegate, and GoRouter) struggle with several fundamental limitations due to Flutter's monolithic canvas model. Neon resolves these challenges natively:
| Dimension | Flutter (Navigator 2.0 / GoRouter) | Neon Navigation Engine |
| :--- | :--- | :--- |
| History Serialization | Extremely complex. Routes are runtime Dart closures holding active widget instances and animations in memory. | 100% JSON-Serializable. The entire navigation stack, history, and route state are clean, serializable data structures. |
| Native Shell Stack Alignment | Fakes page transitions inside a single OpenGL/Impeller surface. Native gestures often feel non-native or laggy. | Native Shell Coordination. Can drive true native UINavigationController and Android FragmentManager backstacks directly. |
| Predictive Back (Android 13+) | Difficult to coordinate across the engine boundary; prone to visual stutter or gesture desync. | Native-Aware Back Arbitration. Emits canPop metadata to native shells for native Predictive Back and iOS edge-swipe gestures. |
| Reactive State Integration | URL redirects trigger full RouterDelegate rebuild cascades and widget tree re-parsing cycles. | Signal-Driven Micro-Routing. Subscribes directly to NeonSignals; evaluates guards with zero redundant tree churn. |
| Route State Caching | Requires manual, error-prone AutomaticKeepAliveClientMixin implementations. | Declarative Route Retention Policies (disposeImmediately, keepAlive, staleWhileRevalidate, lru). |
| Synthetic Deep Link Stacks | Requires complex manual Uri rewriting or multi-page configuration in GoRouter. | Automatic Ancestor Backstack Synthesis. Fabricates natural parent history entries automatically on deep link ingress. |
2. Declarative Route Trees (NeonRoute)
Routes in Neon are declared as clean hierarchical route trees using NeonRoute:
import 'package:neon_framework/neon.dart';
final routes = [
NeonRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomeScreen(),
),
NeonRoute(
path: '/users/:userId',
name: 'user_profile',
builder: (context, state) {
final userId = state.param('userId');
return UserProfileScreen(userId: userId);
},
),
NeonRoute(
path: '/dashboard',
name: 'dashboard',
builder: (context, state) => const DashboardShell(),
children: [
NeonRoute(
path: 'analytics',
name: 'analytics_tab',
builder: (context, state) => const AnalyticsTab(),
),
NeonRoute(
path: 'reports/:reportId',
name: 'report_detail',
cachePolicy: RouteCachePolicy.keepAlive,
builder: (context, state) {
final reportId = state.param('reportId');
return ReportDetailScreen(reportId: reportId);
},
),
],
),
NeonRoute(
path: '/files/*filepath',
name: 'file_viewer',
builder: (context, state) {
final path = state.param('filepath');
return FileViewerScreen(filePath: path);
},
),
];
Route Path Syntax
Neon's NeonPathMatcher supports comprehensive URL pattern matching:
- Static Segments:
/home,/settings/profile - Dynamic Path Parameters:
/users/:userId(accessible viastate.param('userId')) - Wildcard Catch-All:
/files/*filepath(captures the entire remaining path) - Query Parameters:
/search?q=neon&filter=active(accessible viastate.query('q'))
3. The NeonRouter Hub
NeonRouter acts as the central orchestrator of navigation across the app:
final router = NeonRouter(
routes: routes,
initialLocation: '/',
);
// Initialize router (pushes initialLocation onto root navigator)
await router.initialize();
Context Lookups
Within any widget build function, you can retrieve the router or active navigator controller:
// Retrieve the root router
final router = NeonRouter.of(context);
// Retrieve the nearest navigator controller in the hierarchy
final nav = NavigatorController.of(context);
4. Stack Operations & Awaiting Results
NavigatorController provides complete stack manipulation methods:
Pushing Routes
// Push a screen (resolves once the route is mounted on the stack)
final entry = await router.push('/dashboard/analytics');
// Push with extra arguments
await router.push('/settings', arguments: {'source': 'drawer'});
Awaiting Pop Results (pushForResult)
Unlike Flutter's Navigator.push which forces caller code to pause until pop, Neon provides explicit differentiation:
// 1. Using pushForResult convenience method
final String? confirmed = await router.pushForResult<String>('/confirmation-modal');
// 2. Or listening to entry.onPop directly
final entry = await router.push('/confirmation-modal');
final result = await entry?.onPop;
Popping & Replacing
// Pop top screen and deliver return value
await router.pop('success_result');
// Replace active screen without growing the stack
await router.replace('/home');
// Pop repeatedly until predicate is met
router.rootNavigator.popUntil((entry) => entry.state.location == '/');
5. Route Guards & Middlewares
Neon includes full support for synchronous and asynchronous route guards:
class AuthGuard extends NeonRouteGuard {
final NeonSignal<bool> isAuthenticated;
AuthGuard(this.isAuthenticated);
@override
FutureOr<NeonGuardResult> canActivate(NeonRouteContext context) {
if (isAuthenticated.value) {
return const NeonGuardResult.allow();
}
// Redirect unauthenticated requests to login
return const NeonGuardResult.redirect('/login');
}
@override
FutureOr<NeonGuardResult> canDeactivate(NeonRouteContext context) {
// E.g. Check for unsaved form data
if (hasUnsavedChanges) {
return const NeonGuardResult.abort('Unsaved changes');
}
return const NeonGuardResult.allow();
}
}
Attach guards directly to routes:
NeonRoute(
path: '/admin',
guards: [AuthGuard(authSignal)],
builder: (ctx, state) => const AdminPanel(),
)
Cyclic Redirect Loop Protection
If misconfigured guards trigger circular redirects (e.g. Route A redirects to B, and B redirects to A), Neon's router detects recursion and throws a NavigationRedirectLoopException once depth exceeds 5, protecting the app from lockups.
6. Hierarchical Back-Button & Gesture Handling
Back navigation follows a "Bottom-Up Bubble, Top-Down Intercept" arbitration model:
[Hardware Back / Native Edge Swipe]
│
▼
Is a child tab/modal navigator active?
├─ Yes: Child pops locally
└─ No: Root navigator pops
└─ If root has only 1 screen left:
Delegates to native OS (Minimize / Exit App)
Both Android's OnBackInvokedCallback (Android 13+ Predictive Back) and iOS UIKit interactivePopGestureRecognizer query the router's canPop state to coordinate native transition animations smoothly.
7. Deep Links & Synthetic Backstacks
When an external link opens the app directly to a deep screen (e.g., https://neon.app/dashboard/reports/rpt_99), Neon automatically synthesizes ancestor parent routes:
// Handles universal links and Android intent URIs
await router.handleDeepLink(
'https://neon.app/dashboard/reports/rpt_99',
synthesizeBackstack: true,
);
Synthesized Backstack:
[/] -> [/dashboard] -> [/dashboard/reports/rpt_99] (Active Screen)
When the user taps the back button on the report detail screen, they naturally step back to /dashboard rather than abruptly exiting the app.
8. Reactive Signal-Driven Routing
Neon allows binding NeonSignal instances directly to navigation redirect rules:
final authSignal = NeonSignal<bool>(false);
router.bindSignal(authSignal, redirectRule: () {
return authSignal.value ? null : '/login';
});
Whenever authSignal.value changes to false, Neon's router automatically redirects the user to /login without triggering full widget rebuild cascades.
9. State Restoration & Time-Travel Debugging
Because Neon's navigation stack is 100% JSON-serializable, full session history can be saved to persistent storage and restored across process death:
// Export snapshot before app enters background
final snapshot = router.exportSnapshot();
await kvStore.setString('nav_session', jsonEncode(snapshot));
// Restore on subsequent app launch
final saved = kvStore.getString('nav_session');
if (saved != null) {
await router.restoreFromSnapshot(jsonDecode(saved));
}
Exported Snapshot Schema
{
"restorationVersion": 1,
"timestamp": 1726974152000,
"rootNavigator": {
"navigatorId": "root_nav",
"canPop": true,
"activeIndex": 1,
"history": [
{
"routeId": "root_nav_r1",
"path": "/",
"matchedPath": "/",
"state": { "location": "/", "pathParameters": {}, "queryParameters": {} }
},
{
"routeId": "root_nav_r2",
"path": "/users/42",
"matchedPath": "/users/:userId",
"state": { "location": "/users/42", "pathParameters": { "userId": "42" }, "queryParameters": {} }
}
]
}
}
10. Native Bridge Protocol
When running alongside Android Kotlin or iOS UIKit shells, navigation events can be dispatched via HTTP POST /action:
// Dispatched by native shell when user taps back
{
"action": "nav_pop"
}
// Dispatched by native shell on deep link or push notification
{
"action": "deep_link",
"url": "neonapp://orders/102",
"source": "push_notification"
}
// Dispatched to trigger programmatic navigation
{
"action": "nav_push",
"path": "/settings",
"arguments": { "theme": "dark" }
}
Neon's router.handleBridgeAction(params) processes these payloads asynchronously and updates the active widget tree.