Go router navigation observer
go_router is one of the most popular package for managing navigation in a Flutter application, and since it became part of the Flutter team, it has become the main recommended solution. It's very easy to use, yet feature-rich. However, with the introduction of ShellRoute, observing the navigation flow in your app and having routes that are "aware" of navigation changes has become more difficult. As highlighted in this issue, when you add a ShellRoute, the app loses reactivity to route changes: the NavigationObserver is no longer triggered. While waiting for an official fix or built-in solution from the Flutter team, we can still implement route-aware behavior by listening to the GoRouterDelegate. The following code listens to the GoRouterDelegate throughout the widget's lifecycle and uses internal state along with top-route comparison to expose callbacks in response to navigation events. Of course, this can be extended to fit your needs—for example, reacting to query parameter changes and more. import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; /// A mixin that allows a widget to be aware of the current route. mixin GoRouterAware on State { /// The route to be aware of. late final Uri _observerLocation; /// The current state of the [_observerLocation]. late _GoRouterAwareState _state; /// go router delegate. late GoRouterDelegate _delegate; /// The context of the widget. late BuildContext _context; /// The location of the top route Uri? _currentLocation; @override void initState() { _context = context; final router = GoRouter.of(_context); _state = _GoRouterAwareState.topRoute; _observerLocation = router.state.uri; _delegate = router.routerDelegate; _onChange(); _delegate.addListener(_onChange); super.initState(); } @override void didChangeDependencies() { _context = context; super.didChangeDependencies(); } void _onChange() { _currentLocation = GoRouter.of(_context).state.uri; if (_currentLocation == null) { return; } /// If the current route is the top route and the current location is the same as the observer location then [_observerLocation] is the top route. if (_state.isTopRoute && _sameLocation(_currentLocation!, _observerLocation)) { didPush(); return; } /// If the current route is pushed next and the current location is the same as the observer location then [_observerLocation] is returned to the top route. if (_state.isPushedNext && _sameLocation(_currentLocation!, _observerLocation)) { didPopNext(); _state = _GoRouterAwareState.topRoute; return; } /// If the current route is not the top route and the current location contains the observer location then [_observerLocation] is no longer the top route. if (!_sameLocation(_currentLocation!, _observerLocation) && _currentLocation!.path.toString().contains(_observerLocation.path)) { _state = _GoRouterAwareState.pushedNext; didPushNext(); return; } /// If the current route is the top route and the current location does not contain the observer location then [_observerLocation] is popped off. if (_state.isTopRoute && !_currentLocation!.path.toString().contains(_observerLocation.path)) { didPop(); _state = _GoRouterAwareState.poppedOff; return; } } /// Check if two locations have the same path. bool _sameLocation(Uri a, Uri b) { return a.path.toString() == b.path.toString(); } /// Called when the top route has been popped off, and the current route /// shows up. void didPopNext() {} /// Called when the current route has been pushed. void didPush() {} /// Called when the current route has been popped off. void didPop() {} /// Called when a new route has been pushed, and the current route is no /// longer visible. void didPushNext() {} @override void dispose() { _delegate.removeListener(_onChange); super.dispose(); } } enum _GoRouterAwareState { pushedNext, topRoute, poppedOff; bool get isTopRoute => this == topRoute; bool get isPushedNext => this == pushedNext; bool get isPoppedOff => this == poppedOff; } github gist link.

go_router is one of the most popular package for managing navigation in a Flutter application, and since it became part of the Flutter team, it has become the main recommended solution.
It's very easy to use, yet feature-rich.
However, with the introduction of ShellRoute
, observing the navigation flow in your app and having routes that are "aware" of navigation changes has become more difficult. As highlighted in this issue, when you add a ShellRoute
, the app loses reactivity to route changes: the NavigationObserver
is no longer triggered.
While waiting for an official fix or built-in solution from the Flutter team, we can still implement route-aware behavior by listening to the GoRouterDelegate
.
The following code listens to the GoRouterDelegate
throughout the widget's lifecycle and uses internal state along with top-route comparison to expose callbacks in response to navigation events. Of course, this can be extended to fit your needs—for example, reacting to query parameter changes and more.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// A mixin that allows a widget to be aware of the current route.
mixin GoRouterAware<T extends StatefulWidget> on State<T> {
/// The route to be aware of.
late final Uri _observerLocation;
/// The current state of the [_observerLocation].
late _GoRouterAwareState _state;
/// go router delegate.
late GoRouterDelegate _delegate;
/// The context of the widget.
late BuildContext _context;
/// The location of the top route
Uri? _currentLocation;
@override
void initState() {
_context = context;
final router = GoRouter.of(_context);
_state = _GoRouterAwareState.topRoute;
_observerLocation = router.state.uri;
_delegate = router.routerDelegate;
_onChange();
_delegate.addListener(_onChange);
super.initState();
}
@override
void didChangeDependencies() {
_context = context;
super.didChangeDependencies();
}
void _onChange() {
_currentLocation = GoRouter.of(_context).state.uri;
if (_currentLocation == null) {
return;
}
/// If the current route is the top route and the current location is the same as the observer location then [_observerLocation] is the top route.
if (_state.isTopRoute &&
_sameLocation(_currentLocation!, _observerLocation)) {
didPush();
return;
}
/// If the current route is pushed next and the current location is the same as the observer location then [_observerLocation] is returned to the top route.
if (_state.isPushedNext &&
_sameLocation(_currentLocation!, _observerLocation)) {
didPopNext();
_state = _GoRouterAwareState.topRoute;
return;
}
/// If the current route is not the top route and the current location contains the observer location then [_observerLocation] is no longer the top route.
if (!_sameLocation(_currentLocation!, _observerLocation) &&
_currentLocation!.path.toString().contains(_observerLocation.path)) {
_state = _GoRouterAwareState.pushedNext;
didPushNext();
return;
}
/// If the current route is the top route and the current location does not contain the observer location then [_observerLocation] is popped off.
if (_state.isTopRoute &&
!_currentLocation!.path.toString().contains(_observerLocation.path)) {
didPop();
_state = _GoRouterAwareState.poppedOff;
return;
}
}
/// Check if two locations have the same path.
bool _sameLocation(Uri a, Uri b) {
return a.path.toString() == b.path.toString();
}
/// Called when the top route has been popped off, and the current route
/// shows up.
void didPopNext() {}
/// Called when the current route has been pushed.
void didPush() {}
/// Called when the current route has been popped off.
void didPop() {}
/// Called when a new route has been pushed, and the current route is no
/// longer visible.
void didPushNext() {}
@override
void dispose() {
_delegate.removeListener(_onChange);
super.dispose();
}
}
enum _GoRouterAwareState {
pushedNext,
topRoute,
poppedOff;
bool get isTopRoute => this == topRoute;
bool get isPushedNext => this == pushedNext;
bool get isPoppedOff => this == poppedOff;
}