Skip to content

Commit 959c653

Browse files
authored
feat(routing): add NavigatorObserver support to MagicRouter (#34)
* feat(routing): add NavigatorObserver support to MagicRouter (#31) MagicRouter.instance.addObserver() enables Sentry, Firebase Analytics, and custom NavigatorObserver integration. Observers are passed to GoRouter automatically. Registration enforces same pre-build guard as routes. * fix(test): rename misleading observer test — GoRouter doesn't expose observers (#34) Renamed 'observers are passed to GoRouter' → 'observers persist after router build' since GoRouter doesn't publicly expose its observer list.
1 parent bea108c commit 959c653

7 files changed

Lines changed: 135 additions & 1 deletion

File tree

.claude/rules/routing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ path: "lib/src/routing/**/*.dart"
1717
- `LayoutDefinition` — shell route wrapper for persistent navigation (sidebar, bottom nav)
1818
- `RouteServiceProvider` registers routes in boot phase — recommended place for all route definitions
1919
- Custom transitions: `Route.get('/path', () => Page()).transition(TransitionType.fade)`
20+
- Observer support: `MagicRouter.instance.addObserver(observer)` — must register before `routerConfig` is accessed. Passed to GoRouter `observers` param. Read-only via `observers` getter

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

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

5+
## [Unreleased]
6+
7+
### ✨ Features
8+
- **Router Observers**: `MagicRouter.instance.addObserver()` enables NavigatorObserver integration for analytics/monitoring (Sentry, Firebase Analytics, custom observers). Observers are passed to GoRouter automatically. (#31)
9+
510
## [1.0.0-alpha.6] - 2026-04-05
611

712
### ✨ Features

doc/basics/routing.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- [Layouts (Shell Routes)](#layouts-shell-routes)
1212
- [Context-Free Navigation](#context-free-navigation)
1313
- [Route Middleware](#route-middleware)
14+
- [Navigator Observers](#navigator-observers)
1415

1516
<a name="introduction"></a>
1617
## Introduction
@@ -310,3 +311,37 @@ MagicRoute.page('/admin', () => AdminPanel())
310311
```
311312

312313
See the [Middleware documentation](/basics/middleware) for details on creating custom middleware.
314+
315+
<a name="navigator-observers"></a>
316+
## Navigator Observers
317+
318+
Register `NavigatorObserver` instances for analytics, monitoring, or performance tracking. Observers must be added before the router is built (typically in your `RouteServiceProvider`):
319+
320+
```dart
321+
class RouteServiceProvider extends ServiceProvider {
322+
@override
323+
Future<void> boot() async {
324+
// Add observers before registering routes
325+
MagicRouter.instance.addObserver(SentryNavigatorObserver(
326+
enableAutoTransactions: true,
327+
setRouteNameAsTransaction: true,
328+
));
329+
330+
MagicRouter.instance.addObserver(FirebaseAnalyticsObserver(
331+
analytics: FirebaseAnalytics.instance,
332+
));
333+
334+
registerAppRoutes();
335+
}
336+
337+
void registerAppRoutes() {
338+
MagicRoute.page('/', () => HomePage());
339+
// ...
340+
}
341+
}
342+
```
343+
344+
Observers are passed directly to GoRouter and receive all navigation events (`didPush`, `didPop`, `didReplace`, `didRemove`).
345+
346+
> [!NOTE]
347+
> Observers must be registered before `routerConfig` is accessed. Adding observers after the router is built throws a `StateError`.

lib/src/routing/magic_router.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ class MagicRouter {
6868
/// Registered layout definitions.
6969
final List<LayoutDefinition> _layouts = [];
7070

71+
/// Registered navigator observers.
72+
final List<NavigatorObserver> _observers = [];
73+
7174
/// The built GoRouter instance (lazily created).
7275
GoRouter? _router;
7376

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

150+
/// Get all registered navigator observers.
151+
List<NavigatorObserver> get observers => List.unmodifiable(_observers);
152+
153+
/// Add a navigator observer.
154+
///
155+
/// Must be called before the router is built (before [routerConfig] is accessed).
156+
void addObserver(NavigatorObserver observer) {
157+
if (_isBuilt) {
158+
throw StateError(
159+
'Cannot add observers after the router has been built. '
160+
'Register all observers before accessing routerConfig.',
161+
);
162+
}
163+
_observers.add(observer);
164+
}
165+
147166
// ---------------------------------------------------------------------------
148167
// Router Configuration
149168
// ---------------------------------------------------------------------------
@@ -182,6 +201,7 @@ class MagicRouter {
182201
return GoRouter(
183202
navigatorKey: navigatorKey,
184203
initialLocation: _initialLocation,
204+
observers: _observers,
185205
routes: _buildRoutes(),
186206
redirect: _handleRedirect,
187207
onException: (context, state, router) {
@@ -603,6 +623,7 @@ class MagicRouter {
603623
static void reset() {
604624
_instance?._routes.clear();
605625
_instance?._layouts.clear();
626+
_instance?._observers.clear();
606627
_instance?._router = null;
607628
_instance?._isBuilt = false;
608629
_instance?._intendedUrl = null;

skills/magic-framework/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ Official plugins extending Magic Framework. Each has its own package, service pr
620620
| `references/eloquent-orm.md` | Model definition, attributes, casts, relations, `InteractsWithPersistence`, QueryBuilder, migrations, Blueprint | Working with models, database queries, or migrations |
621621
| `references/controllers-views.md` | MagicController, MagicStateMixin, RxStatus, MagicView, MagicStatefulView, MagicStatefulViewState, MagicResponsiveView, MagicBuilder, MagicCan/MagicCannot | Building controllers or views, reactive state, authorization widgets |
622622
| `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 |
623-
| `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 |
623+
| `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 |
624624
| `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 |
625625
| `references/auth-system.md` | Auth facade, AuthManager, guards (Bearer, BasicAuth, ApiKey), token refresh, setUserFactory, restore, policies, Gate, MagicCan | Implementing authentication, authorization, or token management |
626626
| `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 |

skills/magic-framework/references/routing-navigation.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,33 @@ MagicRoute.layout(
420420
);
421421
```
422422

423+
## Navigator Observers
424+
425+
Register `NavigatorObserver` instances for analytics, monitoring, or performance tracking. Observers must be registered before the router is built.
426+
427+
```dart
428+
// In RouteServiceProvider.boot()
429+
MagicRouter.instance.addObserver(SentryNavigatorObserver(
430+
enableAutoTransactions: true,
431+
setRouteNameAsTransaction: true,
432+
));
433+
434+
MagicRouter.instance.addObserver(FirebaseAnalyticsObserver(
435+
analytics: FirebaseAnalytics.instance,
436+
));
437+
```
438+
439+
Read-only access to registered observers:
440+
441+
```dart
442+
final observers = MagicRouter.instance.observers; // List<NavigatorObserver> (unmodifiable)
443+
```
444+
445+
Observers are passed to GoRouter's `observers` parameter automatically. Adding observers after `routerConfig` is accessed throws `StateError`.
446+
423447
## Gotchas
424448

449+
- **Observer Registration Timing:** Observers must be added before `routerConfig` is accessed, same as routes. Register in `RouteServiceProvider.boot()`.
425450
- **Route Registration Timing:** Routes must be registered during `ServiceProvider.register()` or `boot()`. They cannot be added after `MagicRouter.instance.routerConfig` is accessed.
426451
- **Middleware Next Required:** Middleware must call `next()` to allow the request to proceed. Failing to call it halts the pipeline.
427452
- **Path Parameters:** Parameters are injected by position into the handler function. Ensure the function signature matches the number of parameters in the route.

test/routing/router_test.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,50 @@ void main() {
345345
});
346346
});
347347

348+
group('Observer Support', () {
349+
test('addObserver() stores observer', () {
350+
final observer = _TestNavigatorObserver();
351+
352+
MagicRouter.instance.addObserver(observer);
353+
354+
expect(MagicRouter.instance.observers, contains(observer));
355+
});
356+
357+
test('observers persist after router build', () {
358+
final observer = _TestNavigatorObserver();
359+
360+
MagicRouter.instance.addObserver(observer);
361+
MagicRoute.page('/', () => const SizedBox());
362+
363+
final config = MagicRouter.instance.routerConfig;
364+
365+
expect(config, isNotNull);
366+
expect(MagicRouter.instance.observers, contains(observer));
367+
});
368+
369+
test('addObserver() throws after build', () {
370+
MagicRoute.page('/', () => const SizedBox());
371+
372+
// Trigger the lazy build.
373+
MagicRouter.instance.routerConfig;
374+
375+
expect(
376+
() => MagicRouter.instance.addObserver(_TestNavigatorObserver()),
377+
throwsA(isA<StateError>()),
378+
);
379+
});
380+
381+
test('reset() clears observers', () {
382+
final observer = _TestNavigatorObserver();
383+
384+
MagicRouter.instance.addObserver(observer);
385+
386+
MagicRouter.reset();
387+
388+
expect(MagicRouter.instance.observers, isEmpty);
389+
});
390+
});
391+
348392
/// History-based back() navigation.
349393
///
350394
/// These tests verify the history tracking feature, including
@@ -590,3 +634,6 @@ class _TestMiddleware extends MagicMiddleware {
590634
@override
591635
Future<void> handle(void Function() next) async => next();
592636
}
637+
638+
/// Test NavigatorObserver implementation.
639+
class _TestNavigatorObserver extends NavigatorObserver {}

0 commit comments

Comments
 (0)