Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/rules/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ path: "lib/src/routing/**/*.dart"
- `LayoutDefinition` — shell route wrapper for persistent navigation (sidebar, bottom nav)
- `RouteServiceProvider` registers routes in boot phase — recommended place for all route definitions
- Custom transitions: `Route.get('/path', () => Page()).transition(TransitionType.fade)`
- Observer support: `MagicRouter.instance.addObserver(observer)` — must register before `routerConfig` is accessed. Passed to GoRouter `observers` param. Read-only via `observers` getter
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

### ✨ Features
- **Router Observers**: `MagicRouter.instance.addObserver()` enables NavigatorObserver integration for analytics/monitoring (Sentry, Firebase Analytics, custom observers). Observers are passed to GoRouter automatically. (#31)

## [1.0.0-alpha.6] - 2026-04-05

### ✨ Features
Expand Down
35 changes: 35 additions & 0 deletions doc/basics/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Layouts (Shell Routes)](#layouts-shell-routes)
- [Context-Free Navigation](#context-free-navigation)
- [Route Middleware](#route-middleware)
- [Navigator Observers](#navigator-observers)

<a name="introduction"></a>
## Introduction
Expand Down Expand Up @@ -310,3 +311,37 @@ MagicRoute.page('/admin', () => AdminPanel())
```

See the [Middleware documentation](/basics/middleware) for details on creating custom middleware.

<a name="navigator-observers"></a>
## Navigator Observers

Register `NavigatorObserver` instances for analytics, monitoring, or performance tracking. Observers must be added before the router is built (typically in your `RouteServiceProvider`):

```dart
class RouteServiceProvider extends ServiceProvider {
@override
Future<void> boot() async {
// Add observers before registering routes
MagicRouter.instance.addObserver(SentryNavigatorObserver(
enableAutoTransactions: true,
setRouteNameAsTransaction: true,
));

MagicRouter.instance.addObserver(FirebaseAnalyticsObserver(
analytics: FirebaseAnalytics.instance,
));

registerAppRoutes();
}

void registerAppRoutes() {
MagicRoute.page('/', () => HomePage());
// ...
}
}
```

Observers are passed directly to GoRouter and receive all navigation events (`didPush`, `didPop`, `didReplace`, `didRemove`).

> [!NOTE]
> Observers must be registered before `routerConfig` is accessed. Adding observers after the router is built throws a `StateError`.
21 changes: 21 additions & 0 deletions lib/src/routing/magic_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ class MagicRouter {
/// Registered layout definitions.
final List<LayoutDefinition> _layouts = [];

/// Registered navigator observers.
final List<NavigatorObserver> _observers = [];

/// The built GoRouter instance (lazily created).
GoRouter? _router;

Expand Down Expand Up @@ -144,6 +147,22 @@ class MagicRouter {
/// Get all registered routes.
List<RouteDefinition> get routes => List.unmodifiable(_routes);

/// Get all registered navigator observers.
List<NavigatorObserver> get observers => List.unmodifiable(_observers);

/// Add a navigator observer.
///
/// Must be called before the router is built (before [routerConfig] is accessed).
void addObserver(NavigatorObserver observer) {
if (_isBuilt) {
throw StateError(
'Cannot add observers after the router has been built. '
'Register all observers before accessing routerConfig.',
);
}
_observers.add(observer);
}

// ---------------------------------------------------------------------------
// Router Configuration
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -182,6 +201,7 @@ class MagicRouter {
return GoRouter(
navigatorKey: navigatorKey,
initialLocation: _initialLocation,
observers: _observers,
routes: _buildRoutes(),
redirect: _handleRedirect,
onException: (context, state, router) {
Expand Down Expand Up @@ -603,6 +623,7 @@ class MagicRouter {
static void reset() {
_instance?._routes.clear();
_instance?._layouts.clear();
_instance?._observers.clear();
_instance?._router = null;
_instance?._isBuilt = false;
_instance?._intendedUrl = null;
Expand Down
2 changes: 1 addition & 1 deletion skills/magic-framework/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ Official plugins extending Magic Framework. Each has its own package, service pr
| `references/eloquent-orm.md` | Model definition, attributes, casts, relations, `InteractsWithPersistence`, QueryBuilder, migrations, Blueprint | Working with models, database queries, or migrations |
| `references/controllers-views.md` | MagicController, MagicStateMixin, RxStatus, MagicView, MagicStatefulView, MagicStatefulViewState, MagicResponsiveView, MagicBuilder, MagicCan/MagicCannot | Building controllers or views, reactive state, authorization widgets |
| `references/forms-validation.md` | MagicFormData, MagicForm, rules(), FormValidator, ValidatesRequests, built-in rules (Required, Email, Min, Max, Confirmed, Same, Accepted), process(), processingListenable | Building forms, adding validation, handling server-side errors |
| `references/routing-navigation.md` | MagicRoute.page(), group(), layout(), navigation (to/back/replace/push/toNamed), middleware, transitions, MagicRouterOutlet, path/query parameters | Defining routes, navigation, or middleware |
| `references/routing-navigation.md` | MagicRoute.page(), group(), layout(), navigation (to/back/replace/push/toNamed), middleware, transitions, MagicRouterOutlet, path/query parameters, navigator observers | Defining routes, navigation, middleware, or observers |
| `references/http-network.md` | Http facade (get/post/put/delete/upload + RESTful resource methods), MagicResponse API, interceptors, network config | Making HTTP requests, handling responses, or configuring network layer |
| `references/auth-system.md` | Auth facade, AuthManager, guards (Bearer, BasicAuth, ApiKey), token refresh, setUserFactory, restore, policies, Gate, MagicCan | Implementing authentication, authorization, or token management |
| `references/secondary-systems.md` | Cache, Events (EventDispatcher, register listeners), Logging, Localization (trans()), Storage, Encryption, Vault, Carbon date helper, Launch, Policies | Using caching, events, logging, i18n, file storage, encryption, or URL launching |
Expand Down
25 changes: 25 additions & 0 deletions skills/magic-framework/references/routing-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,33 @@ MagicRoute.layout(
);
```

## Navigator Observers

Register `NavigatorObserver` instances for analytics, monitoring, or performance tracking. Observers must be registered before the router is built.

```dart
// In RouteServiceProvider.boot()
MagicRouter.instance.addObserver(SentryNavigatorObserver(
enableAutoTransactions: true,
setRouteNameAsTransaction: true,
));

MagicRouter.instance.addObserver(FirebaseAnalyticsObserver(
analytics: FirebaseAnalytics.instance,
));
```

Read-only access to registered observers:

```dart
final observers = MagicRouter.instance.observers; // List<NavigatorObserver> (unmodifiable)
```

Observers are passed to GoRouter's `observers` parameter automatically. Adding observers after `routerConfig` is accessed throws `StateError`.

## Gotchas

- **Observer Registration Timing:** Observers must be added before `routerConfig` is accessed, same as routes. Register in `RouteServiceProvider.boot()`.
- **Route Registration Timing:** Routes must be registered during `ServiceProvider.register()` or `boot()`. They cannot be added after `MagicRouter.instance.routerConfig` is accessed.
- **Middleware Next Required:** Middleware must call `next()` to allow the request to proceed. Failing to call it halts the pipeline.
- **Path Parameters:** Parameters are injected by position into the handler function. Ensure the function signature matches the number of parameters in the route.
Expand Down
47 changes: 47 additions & 0 deletions test/routing/router_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,50 @@ void main() {
});
});

group('Observer Support', () {
test('addObserver() stores observer', () {
final observer = _TestNavigatorObserver();

MagicRouter.instance.addObserver(observer);

expect(MagicRouter.instance.observers, contains(observer));
});

test('observers persist after router build', () {
final observer = _TestNavigatorObserver();

MagicRouter.instance.addObserver(observer);
MagicRoute.page('/', () => const SizedBox());

final config = MagicRouter.instance.routerConfig;

expect(config, isNotNull);
expect(MagicRouter.instance.observers, contains(observer));
});

test('addObserver() throws after build', () {
MagicRoute.page('/', () => const SizedBox());

// Trigger the lazy build.
MagicRouter.instance.routerConfig;

expect(
() => MagicRouter.instance.addObserver(_TestNavigatorObserver()),
throwsA(isA<StateError>()),
);
});

test('reset() clears observers', () {
final observer = _TestNavigatorObserver();

MagicRouter.instance.addObserver(observer);

MagicRouter.reset();

expect(MagicRouter.instance.observers, isEmpty);
});
});

/// History-based back() navigation.
///
/// These tests verify the history tracking feature, including
Expand Down Expand Up @@ -590,3 +634,6 @@ class _TestMiddleware extends MagicMiddleware {
@override
Future<void> handle(void Function() next) async => next();
}

/// Test NavigatorObserver implementation.
class _TestNavigatorObserver extends NavigatorObserver {}