Skip to content

Commit 8176ffd

Browse files
[go_router] ShellRoute will merge GoRouter's observers (#9436)
`ShellRoute` currently does not trigger `observers` defined in `GoRouter`, which is unexpected behavior. Fixes flutter/flutter#112196 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent b87777c commit 8176ffd

File tree

8 files changed

+200
-4
lines changed

8 files changed

+200
-4
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 17.0.0
2+
3+
- **BREAKING CHANGE**
4+
- `ShellRoute`'s navigating changes notify `GoRouter`'s observers by default.
5+
- Adds `notifyRootObserver` to `ShellRouteBase`, `ShellRoute`, `StatefulShellRoute`, `ShellRouteData.$route`, `TypedShellRoute`, `TypedStatefulShellRoute`.
6+
17
## 16.3.0
28

39
- Adds a top-level `onEnter` callback with access to current and next route states.

packages/go_router/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ See the API documentation for details on the following topics:
3838
- [State restoration](https://pub.dev/documentation/go_router/latest/topics/State%20restoration-topic.html)
3939

4040
## Migration Guides
41+
- [Migrating to 17.0.0](https://flutter.dev/go/go-router-v17-breaking-changes).
4142
- [Migrating to 16.0.0](https://flutter.dev/go/go-router-v16-breaking-changes).
4243
- [Migrating to 15.0.0](https://flutter.dev/go/go-router-v15-breaking-changes).
4344
- [Migrating to 14.0.0](https://flutter.dev/go/go-router-v14-breaking-changes).

packages/go_router/lib/src/route.dart

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,17 @@ abstract class ShellRouteBase extends RouteBase {
498498
super.redirect,
499499
required super.routes,
500500
required super.parentNavigatorKey,
501+
this.notifyRootObserver = true,
501502
}) : super._();
502503

504+
/// Whether navigation changes will notify the GoRouter's observers.
505+
///
506+
/// When `true`, navigation changes within this shell route will notify
507+
/// the GoRouter's observers.
508+
///
509+
/// Defaults to `true`.
510+
final bool notifyRootObserver;
511+
503512
static void _debugCheckSubRouteParentNavigatorKeys(
504513
List<RouteBase> subRoutes,
505514
GlobalKey<NavigatorState> navigatorKey,
@@ -577,14 +586,28 @@ class ShellRouteContext {
577586
final NavigatorBuilder navigatorBuilder;
578587

579588
Widget _buildNavigatorForCurrentRoute(
589+
BuildContext context,
580590
List<NavigatorObserver>? observers,
591+
bool notifyRootObserver,
581592
String? restorationScopeId,
582593
) {
594+
final List<NavigatorObserver> effectiveObservers = <NavigatorObserver>[
595+
...?observers,
596+
];
597+
598+
if (notifyRootObserver) {
599+
final List<NavigatorObserver>? rootObservers =
600+
GoRouter.maybeOf(context)?.observers;
601+
if (rootObservers != null) {
602+
effectiveObservers.add(_MergedNavigatorObserver(rootObservers));
603+
}
604+
}
605+
583606
return navigatorBuilder(
584607
navigatorKey,
585608
match,
586609
routeMatchList,
587-
observers,
610+
effectiveObservers,
588611
restorationScopeId,
589612
);
590613
}
@@ -691,6 +714,7 @@ class ShellRoute extends ShellRouteBase {
691714
super.redirect,
692715
this.builder,
693716
this.pageBuilder,
717+
super.notifyRootObserver,
694718
this.observers,
695719
required super.routes,
696720
super.parentNavigatorKey,
@@ -732,7 +756,9 @@ class ShellRoute extends ShellRouteBase {
732756
) {
733757
if (builder != null) {
734758
final Widget navigator = shellRouteContext._buildNavigatorForCurrentRoute(
759+
context,
735760
observers,
761+
notifyRootObserver,
736762
restorationScopeId,
737763
);
738764
return builder!(context, state, navigator);
@@ -748,7 +774,9 @@ class ShellRoute extends ShellRouteBase {
748774
) {
749775
if (pageBuilder != null) {
750776
final Widget navigator = shellRouteContext._buildNavigatorForCurrentRoute(
777+
context,
751778
observers,
779+
notifyRootObserver,
752780
restorationScopeId,
753781
);
754782
return pageBuilder!(context, state, navigator);
@@ -870,6 +898,7 @@ class StatefulShellRoute extends ShellRouteBase {
870898
super.redirect,
871899
this.builder,
872900
this.pageBuilder,
901+
super.notifyRootObserver,
873902
required this.navigatorContainerBuilder,
874903
super.parentNavigatorKey,
875904
this.restorationScopeId,
@@ -900,6 +929,7 @@ class StatefulShellRoute extends ShellRouteBase {
900929
/// for a complete runnable example using StatefulShellRoute.indexedStack.
901930
StatefulShellRoute.indexedStack({
902931
required List<StatefulShellBranch> branches,
932+
bool notifyRootObserver = true,
903933
GoRouterRedirect? redirect,
904934
StatefulShellRouteBuilder? builder,
905935
GlobalKey<NavigatorState>? parentNavigatorKey,
@@ -911,6 +941,7 @@ class StatefulShellRoute extends ShellRouteBase {
911941
redirect: redirect,
912942
builder: builder,
913943
pageBuilder: pageBuilder,
944+
notifyRootObserver: notifyRootObserver,
914945
parentNavigatorKey: parentNavigatorKey,
915946
restorationScopeId: restorationScopeId,
916947
navigatorContainerBuilder: _indexedStackContainerBuilder,
@@ -1422,7 +1453,9 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
14221453
previousBranchLocation != currentBranchLocation;
14231454
if (locationChanged || !hasExistingNavigator) {
14241455
branchState.navigator = shellRouteContext._buildNavigatorForCurrentRoute(
1456+
context,
14251457
branch.observers,
1458+
route.notifyRootObserver,
14261459
branch.restorationScopeId,
14271460
);
14281461
}
@@ -1674,3 +1707,67 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget {
16741707
);
16751708
}
16761709
}
1710+
1711+
/// A wrapper that merges multiple [NavigatorObserver]s into a single observer.
1712+
///
1713+
/// This is necessary because a [NavigatorObserver] can only be attached to one
1714+
/// [NavigatorState] at a time.
1715+
class _MergedNavigatorObserver extends NavigatorObserver {
1716+
/// Default constructor for the merged navigator observer.
1717+
_MergedNavigatorObserver(this.observers);
1718+
1719+
/// The observers to be merged.
1720+
final List<NavigatorObserver> observers;
1721+
1722+
@override
1723+
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
1724+
for (final NavigatorObserver observer in observers) {
1725+
observer.didPush(route, previousRoute);
1726+
}
1727+
}
1728+
1729+
@override
1730+
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
1731+
for (final NavigatorObserver observer in observers) {
1732+
observer.didPop(route, previousRoute);
1733+
}
1734+
}
1735+
1736+
@override
1737+
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
1738+
for (final NavigatorObserver observer in observers) {
1739+
observer.didRemove(route, previousRoute);
1740+
}
1741+
}
1742+
1743+
@override
1744+
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
1745+
for (final NavigatorObserver observer in observers) {
1746+
observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
1747+
}
1748+
}
1749+
1750+
@override
1751+
void didChangeTop(Route<dynamic> topRoute, Route<dynamic>? previousTopRoute) {
1752+
for (final NavigatorObserver observer in observers) {
1753+
observer.didChangeTop(topRoute, previousTopRoute);
1754+
}
1755+
}
1756+
1757+
@override
1758+
void didStartUserGesture(
1759+
Route<dynamic> route,
1760+
Route<dynamic>? previousRoute,
1761+
) {
1762+
for (final NavigatorObserver observer in observers) {
1763+
observer.didStartUserGesture(route, previousRoute);
1764+
}
1765+
}
1766+
1767+
@override
1768+
void didStopUserGesture() {
1769+
for (final NavigatorObserver observer in observers) {
1770+
observer.didStopUserGesture();
1771+
}
1772+
}
1773+
}

packages/go_router/lib/src/route_data.dart

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ abstract class ShellRouteData extends RouteData {
314314
GlobalKey<NavigatorState>? navigatorKey,
315315
GlobalKey<NavigatorState>? parentNavigatorKey,
316316
List<RouteBase> routes = const <RouteBase>[],
317+
bool notifyRootObserver = true,
317318
List<NavigatorObserver>? observers,
318319
String? restorationScopeId,
319320
}) {
@@ -342,6 +343,7 @@ abstract class ShellRouteData extends RouteData {
342343
parentNavigatorKey: parentNavigatorKey,
343344
routes: routes,
344345
navigatorKey: navigatorKey,
346+
notifyRootObserver: notifyRootObserver,
345347
observers: observers,
346348
restorationScopeId: restorationScopeId,
347349
redirect: redirect,
@@ -559,7 +561,16 @@ class TypedRelativeGoRoute<T extends RelativeGoRouteData>
559561
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
560562
class TypedShellRoute<T extends ShellRouteData> extends TypedRoute<T> {
561563
/// Default const constructor
562-
const TypedShellRoute({this.routes = const <TypedRoute<RouteData>>[]});
564+
const TypedShellRoute({
565+
this.notifyRootObserver = true,
566+
this.routes = const <TypedRoute<RouteData>>[],
567+
});
568+
569+
/// Whether navigation changes within this shell route will notify the
570+
/// GoRouter's observers.
571+
///
572+
/// See [ShellRouteBase.notifyRootObserver].
573+
final bool notifyRootObserver;
563574

564575
/// Child route definitions.
565576
///
@@ -573,9 +584,16 @@ class TypedStatefulShellRoute<T extends StatefulShellRouteData>
573584
extends TypedRoute<T> {
574585
/// Default const constructor
575586
const TypedStatefulShellRoute({
587+
this.notifyRootObserver = true,
576588
this.branches = const <TypedStatefulShellBranch<StatefulShellBranchData>>[],
577589
});
578590

591+
/// Whether navigation changes within this shell route will notify the
592+
/// GoRouter's observers.
593+
///
594+
/// See [ShellRouteBase.notifyRootObserver].
595+
final bool notifyRootObserver;
596+
579597
/// Child route definitions.
580598
///
581599
/// See [RouteBase.routes].

packages/go_router/lib/src/router.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ class GoRouter implements RouterConfig<RouteMatchList> {
235235
String? initialLocation,
236236
this.overridePlatformDefaultLocation = false,
237237
Object? initialExtra,
238-
List<NavigatorObserver>? observers,
238+
this.observers,
239239
bool debugLogDiagnostics = false,
240240
GlobalKey<NavigatorState>? navigatorKey,
241241
String? restorationScopeId,
@@ -369,6 +369,9 @@ class GoRouter implements RouterConfig<RouteMatchList> {
369369
@override
370370
late final GoRouteInformationParser routeInformationParser;
371371

372+
/// The navigator observers used by [GoRouter].
373+
final List<NavigatorObserver>? observers;
374+
372375
void _handleRoutingConfigChanged() {
373376
// Reparse is needed to update its builder
374377
restore(configuration.reparse(routerDelegate.currentConfiguration));

packages/go_router/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: go_router
22
description: A declarative router for Flutter based on Navigation 2 supporting
33
deep linking, data-driven routes and more
4-
version: 16.3.0
4+
version: 17.0.0
55
repository: https://github.com/flutter/packages/tree/main/packages/go_router
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
77

packages/go_router/test/shell_route_observers_test.dart

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
66
import 'package:flutter_test/flutter_test.dart';
77
import 'package:go_router/go_router.dart';
88

9+
import 'test_helpers.dart';
10+
911
void main() {
1012
test('ShellRoute observers test', () {
1113
final ShellRoute shell = ShellRoute(
@@ -25,4 +27,71 @@ void main() {
2527

2628
expect(shell.observers!.length, 1);
2729
});
30+
31+
testWidgets(
32+
'GoRouter observers should be notified when navigating within ShellRoute',
33+
(WidgetTester tester) async {
34+
final MockObserver observer = MockObserver();
35+
36+
final GlobalKey<NavigatorState> root = GlobalKey<NavigatorState>(
37+
debugLabel: 'root',
38+
);
39+
await createRouter(
40+
<RouteBase>[
41+
GoRoute(path: '/', builder: (_, __) => const Text('Home')),
42+
ShellRoute(
43+
builder: (_, __, Widget child) => child,
44+
routes: <RouteBase>[
45+
GoRoute(path: '/test1', builder: (_, __) => const Text('Test1')),
46+
],
47+
),
48+
StatefulShellRoute.indexedStack(
49+
builder: (_, __, Widget child) => child,
50+
branches: <StatefulShellBranch>[
51+
StatefulShellBranch(
52+
routes: <RouteBase>[
53+
GoRoute(
54+
path: '/test2',
55+
builder: (_, __) => const Text('Test2'),
56+
),
57+
],
58+
),
59+
],
60+
),
61+
],
62+
tester,
63+
navigatorKey: root,
64+
observers: <NavigatorObserver>[observer],
65+
);
66+
await tester.pumpAndSettle();
67+
68+
root.currentContext!.push('/test1');
69+
await tester.pumpAndSettle();
70+
expect(observer.getCallCount('/test1'), 1);
71+
72+
root.currentContext!.push('/test2');
73+
await tester.pumpAndSettle();
74+
expect(observer.getCallCount('/test2'), 1);
75+
},
76+
);
77+
}
78+
79+
class MockObserver extends NavigatorObserver {
80+
final Map<String, int> _callCounts = <String, int>{};
81+
82+
@override
83+
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
84+
final String? routeName = route.settings.name;
85+
if (routeName != null) {
86+
test(routeName);
87+
}
88+
}
89+
90+
void test(String name) {
91+
_callCounts[name] = (_callCounts[name] ?? 0) + 1;
92+
}
93+
94+
int getCallCount(String name) {
95+
return _callCounts[name] ?? 0;
96+
}
2897
}

packages/go_router/test/test_helpers.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ Future<GoRouter> createRouter(
176176
GoExceptionHandler? onException,
177177
bool requestFocus = true,
178178
bool overridePlatformDefaultLocation = false,
179+
List<NavigatorObserver>? observers,
179180
}) async {
180181
final GoRouter goRouter = GoRouter(
181182
routes: routes,
@@ -190,6 +191,7 @@ Future<GoRouter> createRouter(
190191
restorationScopeId: restorationScopeId,
191192
requestFocus: requestFocus,
192193
overridePlatformDefaultLocation: overridePlatformDefaultLocation,
194+
observers: observers,
193195
);
194196
addTearDown(goRouter.dispose);
195197
await tester.pumpWidget(

0 commit comments

Comments
 (0)