Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4e87717
[go_router] `ShellRoute` will merge `GoRouter`'s observers
huanghui1998hhh Jun 17, 2025
962c175
fix: test
huanghui1998hhh Jun 17, 2025
d954fd5
update CHANGELOG.md and pubspec.yaml
huanghui1998hhh Jun 17, 2025
58f3da2
Merge branch 'main' into main
huanghui1998hhh Jun 17, 2025
1bfdc41
Added `observers` property to `GoRouter` to make it easier to access …
huanghui1998hhh Jun 19, 2025
7a44752
Merge remote-tracking branch 'upstream/main'
huanghui1998hhh Jul 23, 2025
821fef8
Merge remote-tracking branch 'upstream/main'
huanghui1998hhh Sep 30, 2025
158fe7d
Merge remote-tracking branch 'upstream/main'
huanghui1998hhh Oct 21, 2025
1cb0a5f
1. hide _MergedNavigatorObserver implementation
huanghui1998hhh Oct 21, 2025
c558677
code format
huanghui1998hhh Oct 21, 2025
f9d6884
update package version
huanghui1998hhh Oct 21, 2025
9a18143
Merge branch 'main' into main
huanghui1998hhh Oct 22, 2025
2b4491c
add test for `StatefulShellRoute`
huanghui1998hhh Oct 22, 2025
45ba7ea
update `README.md`
huanghui1998hhh Oct 22, 2025
f710a94
Update packages/go_router/lib/src/route.dart
huanghui1998hhh Oct 23, 2025
5490208
Update packages/go_router/lib/src/route.dart
huanghui1998hhh Oct 23, 2025
a313e26
Update packages/go_router/lib/src/route.dart
huanghui1998hhh Oct 23, 2025
01b9c4f
Update comments for `ShellRouteBase.notifyRootObserver`
huanghui1998hhh Oct 23, 2025
dc60e90
Remove `StatefulNavigationShell.notifyRootObserver` and use `Stateful…
huanghui1998hhh Oct 23, 2025
4ae512b
code format
huanghui1998hhh Oct 23, 2025
a916dc0
Merge remote-tracking branch 'upstream/main'
huanghui1998hhh Oct 23, 2025
b6214c2
Update CHANGELOG.md
huanghui1998hhh Oct 23, 2025
e661569
update comments for `TypedShellRoute`, `TypedStatefulShellRoute`
huanghui1998hhh Oct 23, 2025
04b510e
Merge branch 'main' into main
huanghui1998hhh Oct 24, 2025
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
5 changes: 5 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 16.2.5

- `ShellRoute` will auto merge the `observers` passed into `GoRouter`.
- Adds `mergeObservers` to `ShellRouteBase`, `ShellRoute`, `StatefulShellRoute`, `ShellRouteData.$route`.

## 16.2.4

- Fix Android Cold Start deep link with empty path losing scheme and authority.
Expand Down
1 change: 1 addition & 0 deletions packages/go_router/lib/go_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export 'src/misc/custom_parameter.dart';
export 'src/misc/errors.dart';
export 'src/misc/extensions.dart';
export 'src/misc/inherited_router.dart';
export 'src/misc/merged_observer.dart';
export 'src/pages/custom_transition_page.dart';
export 'src/parser.dart';
export 'src/route.dart';
Expand Down
61 changes: 61 additions & 0 deletions packages/go_router/lib/src/misc/merged_observer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:flutter/widgets.dart';

/// A [NavigatorObserver] that merges the observers of the current
/// route with the observers of the previous route.
class MergedNavigatorObserver extends NavigatorObserver {
/// Default constructor for the merged navigator observer.
MergedNavigatorObserver(this.observers);

/// The observers to be merged.
final List<NavigatorObserver> observers;

@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
for (final NavigatorObserver observer in observers) {
observer.didPush(route, previousRoute);
}
}

@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
for (final NavigatorObserver observer in observers) {
observer.didPop(route, previousRoute);
}
}

@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
for (final NavigatorObserver observer in observers) {
observer.didRemove(route, previousRoute);
}
}

@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
for (final NavigatorObserver observer in observers) {
observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
}
}

@override
void didChangeTop(Route<dynamic> topRoute, Route<dynamic>? previousTopRoute) {
for (final NavigatorObserver observer in observers) {
observer.didChangeTop(topRoute, previousTopRoute);
}
}

@override
void didStartUserGesture(
Route<dynamic> route, Route<dynamic>? previousRoute) {
for (final NavigatorObserver observer in observers) {
observer.didStartUserGesture(route, previousRoute);
}
}

@override
void didStopUserGesture() {
for (final NavigatorObserver observer in observers) {
observer.didStopUserGesture();
}
}
}
46 changes: 45 additions & 1 deletion packages/go_router/lib/src/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:meta/meta.dart' as meta;

import 'configuration.dart';
import 'match.dart';
import 'misc/merged_observer.dart';
import 'path_utils.dart';
import 'router.dart';
import 'state.dart';
Expand Down Expand Up @@ -498,8 +499,20 @@ abstract class ShellRouteBase extends RouteBase {
super.redirect,
required super.routes,
required super.parentNavigatorKey,
this.mergeObservers = true,
}) : super._();

/// Whether to merge the observers of the shell route's parent with the
/// observers of the shell route.
///
/// When `true`, the observers of the shell route's parent will be merged with
/// the observers of the shell route.
///
/// Only effective for `observers` passed into `GoRouter`.
///
/// Defaults to `false`.
final bool mergeObservers;

static void _debugCheckSubRouteParentNavigatorKeys(
List<RouteBase> subRoutes,
GlobalKey<NavigatorState> navigatorKey,
Expand Down Expand Up @@ -577,14 +590,30 @@ class ShellRouteContext {
final NavigatorBuilder navigatorBuilder;

Widget _buildNavigatorForCurrentRoute(
BuildContext context,
List<NavigatorObserver>? observers,
bool mergeObservers,
String? restorationScopeId,
) {
final List<NavigatorObserver> effectiveObservers = <NavigatorObserver>[
...?observers
];

if (mergeObservers) {
final List<NavigatorObserver>? rootObservers =
GoRouter.maybeOf(context)?.observers;
if (rootObservers != null) {
effectiveObservers.add(MergedNavigatorObserver(
rootObservers,
));
}
}

return navigatorBuilder(
navigatorKey,
match,
routeMatchList,
observers,
effectiveObservers,
restorationScopeId,
);
}
Expand Down Expand Up @@ -691,6 +720,7 @@ class ShellRoute extends ShellRouteBase {
super.redirect,
this.builder,
this.pageBuilder,
super.mergeObservers,
this.observers,
required super.routes,
super.parentNavigatorKey,
Expand Down Expand Up @@ -732,7 +762,9 @@ class ShellRoute extends ShellRouteBase {
) {
if (builder != null) {
final Widget navigator = shellRouteContext._buildNavigatorForCurrentRoute(
context,
observers,
mergeObservers,
restorationScopeId,
);
return builder!(context, state, navigator);
Expand All @@ -748,7 +780,9 @@ class ShellRoute extends ShellRouteBase {
) {
if (pageBuilder != null) {
final Widget navigator = shellRouteContext._buildNavigatorForCurrentRoute(
context,
observers,
mergeObservers,
restorationScopeId,
);
return pageBuilder!(context, state, navigator);
Expand Down Expand Up @@ -870,6 +904,7 @@ class StatefulShellRoute extends ShellRouteBase {
super.redirect,
this.builder,
this.pageBuilder,
super.mergeObservers,
required this.navigatorContainerBuilder,
super.parentNavigatorKey,
this.restorationScopeId,
Expand Down Expand Up @@ -900,6 +935,7 @@ class StatefulShellRoute extends ShellRouteBase {
/// for a complete runnable example using StatefulShellRoute.indexedStack.
StatefulShellRoute.indexedStack({
required List<StatefulShellBranch> branches,
bool mergeObservers = true,
GoRouterRedirect? redirect,
StatefulShellRouteBuilder? builder,
GlobalKey<NavigatorState>? parentNavigatorKey,
Expand All @@ -911,6 +947,7 @@ class StatefulShellRoute extends ShellRouteBase {
redirect: redirect,
builder: builder,
pageBuilder: pageBuilder,
mergeObservers: mergeObservers,
parentNavigatorKey: parentNavigatorKey,
restorationScopeId: restorationScopeId,
navigatorContainerBuilder: _indexedStackContainerBuilder,
Expand Down Expand Up @@ -1018,6 +1055,7 @@ class StatefulShellRoute extends ShellRouteBase {
) => StatefulNavigationShell(
shellRouteContext: shellRouteContext,
router: GoRouter.of(context),
mergeObservers: mergeObservers,
containerBuilder: navigatorContainerBuilder,
);

Expand Down Expand Up @@ -1207,6 +1245,7 @@ class StatefulNavigationShell extends StatefulWidget {
required this.shellRouteContext,
required GoRouter router,
required this.containerBuilder,
this.mergeObservers = true,
}) : assert(shellRouteContext.route is StatefulShellRoute),
_router = router,
currentIndex = _indexOfBranchNavigatorKey(
Expand All @@ -1231,6 +1270,9 @@ class StatefulNavigationShell extends StatefulWidget {
/// Corresponds to the index in the branches field of [StatefulShellRoute].
final int currentIndex;

/// Same to [ShellRoute.mergeObservers].
final bool mergeObservers;

/// The associated [StatefulShellRoute].
StatefulShellRoute get route => shellRouteContext.route as StatefulShellRoute;

Expand Down Expand Up @@ -1422,7 +1464,9 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
previousBranchLocation != currentBranchLocation;
if (locationChanged || !hasExistingNavigator) {
branchState.navigator = shellRouteContext._buildNavigatorForCurrentRoute(
context,
branch.observers,
false,
branch.restorationScopeId,
);
}
Expand Down
15 changes: 14 additions & 1 deletion packages/go_router/lib/src/route_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ abstract class ShellRouteData extends RouteData {
GlobalKey<NavigatorState>? navigatorKey,
GlobalKey<NavigatorState>? parentNavigatorKey,
List<RouteBase> routes = const <RouteBase>[],
bool mergeObservers = true,
List<NavigatorObserver>? observers,
String? restorationScopeId,
}) {
Expand Down Expand Up @@ -342,6 +343,7 @@ abstract class ShellRouteData extends RouteData {
parentNavigatorKey: parentNavigatorKey,
routes: routes,
navigatorKey: navigatorKey,
mergeObservers: mergeObservers,
observers: observers,
restorationScopeId: restorationScopeId,
redirect: redirect,
Expand Down Expand Up @@ -559,7 +561,12 @@ class TypedRelativeGoRoute<T extends RelativeGoRouteData>
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
class TypedShellRoute<T extends ShellRouteData> extends TypedRoute<T> {
/// Default const constructor
const TypedShellRoute({this.routes = const <TypedRoute<RouteData>>[]});
const TypedShellRoute({ this.mergeObservers = true,this.routes = const <TypedRoute<RouteData>>[]});

/// Determines whether the observers should be merged.
///
/// See [ShellRouteBase.mergeObservers].
final bool mergeObservers;

/// Child route definitions.
///
Expand All @@ -573,9 +580,15 @@ class TypedStatefulShellRoute<T extends StatefulShellRouteData>
extends TypedRoute<T> {
/// Default const constructor
const TypedStatefulShellRoute({
this.mergeObservers = true,
this.branches = const <TypedStatefulShellBranch<StatefulShellBranchData>>[],
});

/// Determines whether the observers should be merged.
///
/// See [ShellRouteBase.mergeObservers].
final bool mergeObservers;

/// Child route definitions.
///
/// See [RouteBase.routes].
Expand Down
5 changes: 4 additions & 1 deletion packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class GoRouter implements RouterConfig<RouteMatchList> {
String? initialLocation,
this.overridePlatformDefaultLocation = false,
Object? initialExtra,
List<NavigatorObserver>? observers,
this.observers,
bool debugLogDiagnostics = false,
GlobalKey<NavigatorState>? navigatorKey,
String? restorationScopeId,
Expand Down Expand Up @@ -309,6 +309,9 @@ class GoRouter implements RouterConfig<RouteMatchList> {
@override
late final GoRouteInformationParser routeInformationParser;

/// The navigator observers used by [GoRouter].
final List<NavigatorObserver>? observers;

void _handleRoutingConfigChanged() {
// Reparse is needed to update its builder
restore(configuration.reparse(routerDelegate.currentConfiguration));
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 16.2.4
version: 16.2.5
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

Expand Down
35 changes: 35 additions & 0 deletions packages/go_router/test/shell_route_observers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';

import 'test_helpers.dart';

void main() {
test('ShellRoute observers test', () {
final ShellRoute shell = ShellRoute(
Expand All @@ -25,4 +28,36 @@ void main() {

expect(shell.observers!.length, 1);
});

testWidgets('observers should be merged', (WidgetTester tester) async {
final HeroController observer = HeroController();
final List<NavigatorObserver> observers = <NavigatorObserver>[observer];
addTearDown(observer.dispose);

final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
await createRouter(
<RouteBase>[
ShellRoute(
navigatorKey: navKey,
builder: (_, __, Widget child) => child,
routes: <RouteBase>[
GoRoute(
path: '/',
parentNavigatorKey: navKey,
builder: (_, __) => const Text('Home'),
),
],
),
],
tester,
observers: observers,
);
await tester.pumpAndSettle();

final List<NavigatorObserver> shellRouteObservers =
navKey.currentState!.widget.observers;
final MergedNavigatorObserver mergedObservers =
shellRouteObservers.single as MergedNavigatorObserver;
expect(listEquals(observers, mergedObservers.observers), isTrue);
});
}
2 changes: 2 additions & 0 deletions packages/go_router/test/test_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Future<GoRouter> createRouter(
GoExceptionHandler? onException,
bool requestFocus = true,
bool overridePlatformDefaultLocation = false,
List<NavigatorObserver>? observers,
}) async {
final GoRouter goRouter = GoRouter(
routes: routes,
Expand All @@ -190,6 +191,7 @@ Future<GoRouter> createRouter(
restorationScopeId: restorationScopeId,
requestFocus: requestFocus,
overridePlatformDefaultLocation: overridePlatformDefaultLocation,
observers: observers,
);
addTearDown(goRouter.dispose);
await tester.pumpWidget(
Expand Down