Skip to content

Commit 278b489

Browse files
angjelkomchunhtai
andauthored
[go_router] [shell_route] Add observers parameter (#2664)
* [go_router] [shell_route] Add observers parameter * [go_router] [shell_route] Add observers parameter test * Added Licence for shell_route_observers_test.dart * [go_router] [shell_route] Added type annotation to shell_route_observers_test.dart * [go_router] [shell_route] Use `HeroControllerScope` for nested Navigator * Use the correct HeroController based on the App type. * Cache the HeroController for the nested Navigator. * Clean up previous cache to prevent memory leak. * Added better cache-clearing policy for the HeroController cache. * Fixed Typos Co-authored-by: chunhtai <[email protected]> * Fixed Typos Co-authored-by: chunhtai <[email protected]> * [go_router] [shell_route] Added a better Hero test Credits to @flodaniel! --------- Co-authored-by: chunhtai <[email protected]>
1 parent 9fadbcb commit 278b489

8 files changed

Lines changed: 133 additions & 5 deletions

File tree

packages/go_router/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 6.0.7
2+
3+
- Add observers parameter to the ShellRoute that will be passed to the nested Navigator.
4+
- Use `HeroControllerScope` for nested Navigator that fixes Hero Widgets not animating in Nested Navigator.
5+
16
## 6.0.6
27

38
- Adds `reverseTransitionDuration` to `CustomTransitionPage`

packages/go_router/lib/src/builder.dart

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ class RouteBuilder {
6262
_routeMatchLookUp[page];
6363

6464
// final Map<>
65+
/// Caches a HeroController for the nested Navigator, which solves cases where the
66+
/// Hero Widget animation stops working when navigating.
67+
final Map<GlobalKey<NavigatorState>, HeroController> _goHeroCache =
68+
<GlobalKey<NavigatorState>, HeroController>{};
6569

6670
/// Builds the top-level Navigator for the given [RouteMatchList].
6771
Widget build(
@@ -132,10 +136,10 @@ class RouteBuilder {
132136
bool routerNeglect,
133137
GlobalKey<NavigatorState> navigatorKey,
134138
Map<Page<Object?>, GoRouterState> registry) {
139+
final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
140+
<GlobalKey<NavigatorState>, List<Page<Object?>>>{};
135141
try {
136142
assert(_routeMatchLookUp.isEmpty);
137-
final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
138-
<GlobalKey<NavigatorState>, List<Page<Object?>>>{};
139143
_buildRecursive(context, matchList, 0, onPopPage, routerNeglect,
140144
keyToPage, navigatorKey, registry);
141145

@@ -147,6 +151,10 @@ class RouteBuilder {
147151
return <Page<Object?>>[
148152
_buildErrorPage(context, e, matchList.uri),
149153
];
154+
} finally {
155+
/// Clean up previous cache to prevent memory leak.
156+
_goHeroCache.removeWhere(
157+
(GlobalKey<NavigatorState> key, _) => !keyToPage.keys.contains(key));
150158
}
151159
}
152160

@@ -191,6 +199,10 @@ class RouteBuilder {
191199
// The key to provide to the ShellRoute's Navigator.
192200
final GlobalKey<NavigatorState> shellNavigatorKey = route.navigatorKey;
193201

202+
// The observers list for the ShellRoute's Navigator.
203+
final List<NavigatorObserver> observers =
204+
route.observers ?? <NavigatorObserver>[];
205+
194206
// Add an entry for the parent navigator if none exists.
195207
keyToPages.putIfAbsent(parentNavigatorKey, () => <Page<Object?>>[]);
196208

@@ -206,9 +218,15 @@ class RouteBuilder {
206218
_buildRecursive(context, matchList, startIndex + 1, onPopPage,
207219
routerNeglect, keyToPages, shellNavigatorKey, registry);
208220

221+
final HeroController heroController = _goHeroCache.putIfAbsent(
222+
shellNavigatorKey, () => _getHeroController(context));
209223
// Build the Navigator
210-
final Widget child = _buildNavigator(
211-
onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey);
224+
final Widget child = HeroControllerScope(
225+
controller: heroController,
226+
child: _buildNavigator(
227+
onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey,
228+
observers: observers),
229+
);
212230

213231
// Build the Page for this route
214232
final Page<Object?> page =
@@ -451,6 +469,18 @@ class RouteBuilder {
451469
: _errorBuilderForAppType!(context, state),
452470
);
453471
}
472+
473+
/// Return a HeroController based on the app type.
474+
HeroController _getHeroController(BuildContext context) {
475+
if (context is Element) {
476+
if (isMaterialApp(context)) {
477+
return createMaterialHeroController();
478+
} else if (isCupertinoApp(context)) {
479+
return createCupertinoHeroController();
480+
}
481+
}
482+
return HeroController();
483+
}
454484
}
455485

456486
typedef _PageBuilderForAppType = Page<void> Function({

packages/go_router/lib/src/pages/cupertino.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import '../misc/extensions.dart';
1111
bool isCupertinoApp(Element elem) =>
1212
elem.findAncestorWidgetOfExactType<CupertinoApp>() != null;
1313

14+
/// Creates a Cupertino HeroController.
15+
HeroController createCupertinoHeroController() =>
16+
CupertinoApp.createCupertinoHeroController();
17+
1418
/// Builds a Cupertino page.
1519
CupertinoPage<void> pageBuilderForCupertinoApp({
1620
required LocalKey key,

packages/go_router/lib/src/pages/material.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import '../misc/extensions.dart';
1212
bool isMaterialApp(Element elem) =>
1313
elem.findAncestorWidgetOfExactType<MaterialApp>() != null;
1414

15+
/// Creates a Material HeroController.
16+
HeroController createMaterialHeroController() =>
17+
MaterialApp.createMaterialHeroController();
18+
1519
/// Builds a Material page.
1620
MaterialPage<void> pageBuilderForMaterialApp({
1721
required LocalKey key,

packages/go_router/lib/src/route.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ class ShellRoute extends RouteBase {
420420
ShellRoute({
421421
this.builder,
422422
this.pageBuilder,
423+
this.observers,
423424
super.routes,
424425
GlobalKey<NavigatorState>? navigatorKey,
425426
}) : assert(routes.isNotEmpty),
@@ -447,6 +448,12 @@ class ShellRoute extends RouteBase {
447448
/// sub-route's builder.
448449
final ShellRoutePageBuilder? pageBuilder;
449450

451+
/// The observers for a shell route.
452+
///
453+
/// The observers parameter is used by the [Navigator] built for this route.
454+
/// sub-route's observers.
455+
final List<NavigatorObserver>? observers;
456+
450457
/// The [GlobalKey] to be used by the [Navigator] built for this route.
451458
/// All ShellRoutes build a Navigator by default. Child GoRoutes
452459
/// are placed onto this Navigator instead of the root Navigator.

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: 6.0.6
4+
version: 6.0.7
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/go_router_test.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3218,6 +3218,56 @@ void main() {
32183218
final bool? result = await resultFuture;
32193219
expect(result, isTrue);
32203220
});
3221+
3222+
testWidgets('Triggers a Hero inside a ShellRoute',
3223+
(WidgetTester tester) async {
3224+
final UniqueKey heroKey = UniqueKey();
3225+
const String kHeroTag = 'hero';
3226+
3227+
final List<RouteBase> routes = <RouteBase>[
3228+
ShellRoute(
3229+
builder: (BuildContext context, GoRouterState state, Widget child) {
3230+
return child;
3231+
},
3232+
routes: <GoRoute>[
3233+
GoRoute(
3234+
path: '/a',
3235+
builder: (BuildContext context, _) {
3236+
return Hero(
3237+
tag: kHeroTag,
3238+
child: Container(),
3239+
flightShuttleBuilder: (_, __, ___, ____, _____) {
3240+
return Container(key: heroKey);
3241+
},
3242+
);
3243+
}),
3244+
GoRoute(
3245+
path: '/b',
3246+
builder: (BuildContext context, _) {
3247+
return Hero(
3248+
tag: kHeroTag,
3249+
child: Container(),
3250+
);
3251+
}),
3252+
],
3253+
)
3254+
];
3255+
final GoRouter router =
3256+
await createRouter(routes, tester, initialLocation: '/a');
3257+
3258+
// check that flightShuttleBuilder widget is not yet present
3259+
expect(find.byKey(heroKey), findsNothing);
3260+
3261+
// start navigation
3262+
router.go('/b');
3263+
await tester.pump();
3264+
await tester.pump(const Duration(milliseconds: 10));
3265+
// check that flightShuttleBuilder widget is visible
3266+
expect(find.byKey(heroKey), isOnstage);
3267+
// // Waits for the animation finishes.
3268+
await tester.pumpAndSettle();
3269+
expect(find.byKey(heroKey), findsNothing);
3270+
});
32213271
});
32223272
});
32233273
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:go_router/go_router.dart';
8+
9+
void main() {
10+
test('ShellRoute observers test', () {
11+
final ShellRoute shell = ShellRoute(
12+
observers: <NavigatorObserver>[HeroController()],
13+
builder: (BuildContext context, GoRouterState state, Widget child) {
14+
return SafeArea(child: child);
15+
},
16+
routes: <RouteBase>[
17+
GoRoute(
18+
path: '/home',
19+
builder: (BuildContext context, GoRouterState state) {
20+
return Container();
21+
},
22+
),
23+
],
24+
);
25+
26+
expect(shell.observers!.length, 1);
27+
});
28+
}

0 commit comments

Comments
 (0)