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;
}
Top comments (0)