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 via state.param('userId'))
  • Wildcard Catch-All: /files/*filepath (captures the entire remaining path)
  • Query Parameters: /search?q=neon&filter=active (accessible via state.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.