Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[go_router] Add ShellRoute #2453

Merged
merged 94 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from 93 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
977f6bc
Add ShellRoute support
johnpryan Jul 22, 2022
2db67e6
Address code review comments
johnpryan Aug 17, 2022
9ffd006
Add pageBuilder to ShellRoute
johnpryan Aug 17, 2022
4da1fa0
Add child route to ShellRoute doc comment
johnpryan Aug 17, 2022
100b33e
Apply suggestions from code review
johnpryan Aug 17, 2022
2387883
Apply suggestions from code review
johnpryan Aug 17, 2022
0f75672
Remove path and other fields from ShellRoute, disable ShellRoute code
johnpryan Aug 22, 2022
4cdb183
Make ShellRoute not require a path
johnpryan Aug 23, 2022
a15b50b
Provide a unique pageKey for ShellRoute
johnpryan Aug 24, 2022
93ca770
add copyright header
johnpryan Aug 24, 2022
3ca5bf4
Fix GoRouteMatch pageKey for ShellRoute
johnpryan Aug 24, 2022
608f83b
Add test, fix sample, add restorationScopeId
johnpryan Aug 25, 2022
dcab29d
Simplify how pages are added to pagesForOutOfScopeNavigator
johnpryan Aug 25, 2022
469745a
Merge branch 'main' of github.com:flutter/packages into shell-route
johnpryan Aug 25, 2022
f9f8c30
Fix analysis warnings
johnpryan Aug 25, 2022
0436f1a
add Nested Navigation section to README
johnpryan Aug 25, 2022
4bc4330
format
johnpryan Aug 25, 2022
8eaf4e2
Remove redundant comment
johnpryan Aug 25, 2022
d92ce85
rename AppScaffold to ScaffoldWithNavBar
johnpryan Aug 25, 2022
c794b87
add explanation to shell route example
johnpryan Aug 25, 2022
cc27334
Throw error if push() contains a ShellRoute
johnpryan Aug 25, 2022
0028006
Handle pop correctly when there is a ShellRoute with one child.
johnpryan Aug 29, 2022
3a70fa5
Fix popRoute implementation
johnpryan Aug 29, 2022
183eb6a
Merge branch 'main' of github.com:flutter/packages into shell-route
johnpryan Aug 29, 2022
3c9d725
Fix conflicts with queryParametersAll feature
johnpryan Aug 29, 2022
340a6ca
format
johnpryan Aug 29, 2022
d2fdeb9
Merge remote-tracking branch 'remotes/origin/main' into shell-route
johnpryan Aug 30, 2022
c8deee5
Apply suggestions from code review
johnpryan Aug 30, 2022
c3c8f66
Update comments
johnpryan Aug 30, 2022
2c4585b
Update packages/go_router/README.md
johnpryan Aug 30, 2022
92ed4ae
update README
johnpryan Aug 30, 2022
41f5b06
Fix analysis issues
johnpryan Aug 30, 2022
780483a
Merge remote-tracking branch 'remotes/origin/main' into shell-route
johnpryan Sep 1, 2022
f064d72
Refactor _buildRecursive to use a Map
johnpryan Sep 1, 2022
0819d4d
rename GoRouteMatch back to RouteMatch
johnpryan Sep 1, 2022
25e3236
Add link to README
johnpryan Sep 2, 2022
de67875
Share code between buildPage and tryBuild
johnpryan Sep 2, 2022
bf1bad0
use buildPages in tryBuild
johnpryan Sep 2, 2022
f82248f
Insert shell route key instead of merging maps
johnpryan Sep 2, 2022
44b8afd
Throw an exception if child is null
johnpryan Sep 2, 2022
02fe72e
make buildPageForRoute private
johnpryan Sep 2, 2022
631fd10
make callRouteBuilder private
johnpryan Sep 2, 2022
b4b4ac8
thow exception instead of error
johnpryan Sep 2, 2022
2d067d7
Add _PageBuilderForAppType typedef
johnpryan Sep 2, 2022
029ad91
make buildErrorNavigator private
johnpryan Sep 2, 2022
d4bb46a
make buildErrorPage private
johnpryan Sep 2, 2022
fb89778
make RouteBuilderException and RouteBuilderError private
johnpryan Sep 2, 2022
61ebdb0
use i-=1 instead of i--
johnpryan Sep 2, 2022
46609b0
opt out of null safety for navigatorKey.currentState
johnpryan Sep 2, 2022
2712c11
Revert rename to GoRouterState subloc and fullpath
johnpryan Sep 2, 2022
f38fa49
Merge branch 'main' into shell-route
johnpryan Sep 8, 2022
692bc46
Add ignore: unused_element
johnpryan Sep 8, 2022
c70f33c
Rename to _debugAssertNotEmpty
johnpryan Sep 8, 2022
e304529
Throw exception if top route is not a GoRoute during redirection
johnpryan Sep 8, 2022
55677e9
Move docstring to class
johnpryan Sep 8, 2022
6248077
Use a single sentence for the first line of ShellRoute doc comment
johnpryan Sep 8, 2022
9378fd5
Add sample code for ShellRoute
johnpryan Sep 9, 2022
de8f97c
[go_router] Fix named routing when using ShellRoutes
jfri Sep 8, 2022
429d08b
[go_router] Add `buildPageWithState` (#2444)
ValentinVignal Sep 6, 2022
a86b8fe
Merge branch 'main' of github.com:flutter/packages into shell-route
johnpryan Sep 9, 2022
9be299a
Assert ShellRoute sub-routes are not empty
johnpryan Sep 9, 2022
813695b
Add Android back button test for ShellRoute
johnpryan Sep 9, 2022
8749762
Add canPop tests
johnpryan Sep 12, 2022
85a1a6d
Add configuration test for navigator keys
johnpryan Sep 12, 2022
424afa4
fix analysis warnings
johnpryan Sep 12, 2022
cc28a23
Update pubspec.yaml
johnpryan Sep 12, 2022
f564e61
Add link to ShellRoute API documentation
johnpryan Sep 12, 2022
e7e795a
Remove RouteConfigurationError, use assertions instead
johnpryan Sep 12, 2022
6c7867a
Add test for empty ShellRoute with empty routes list
johnpryan Sep 12, 2022
73f4747
Assert that parentNavigatorKey configuration is correct
johnpryan Sep 14, 2022
d35a25b
Update canPop and maybePop, add a test
johnpryan Sep 14, 2022
0cf265a
Use assert for asserting the last route match contains a GoRoute
johnpryan Sep 14, 2022
1288344
use variable for last GoRoute in match list
johnpryan Sep 14, 2022
50a31f0
format
johnpryan Sep 14, 2022
3a9f206
Use assert when shellroute has no children
johnpryan Sep 15, 2022
bf54d73
Move log out of assert
johnpryan Sep 15, 2022
e9ff347
use assert for configuration checks
johnpryan Sep 15, 2022
c0794a4
Add test
johnpryan Sep 15, 2022
c6bbccd
remove RedirectionException
johnpryan Sep 15, 2022
9d109db
move GoError to separate file
johnpryan Sep 15, 2022
886bee7
Add newline
johnpryan Sep 15, 2022
26e0515
Update ShellRouteBuilder doc comment
johnpryan Sep 15, 2022
27fb0f5
Update ShellRoutePageBuilder doc comment
johnpryan Sep 15, 2022
e3d31f8
Check that ShellRoute children are a GoRoute and contain parentNaviga…
johnpryan Sep 15, 2022
146ede5
Remove late keyword
johnpryan Sep 15, 2022
73899f2
Add copyright header
johnpryan Sep 15, 2022
97584c3
remove RouteMatcher.canPop()
johnpryan Sep 15, 2022
adc610d
wrap calls to canPop in an assert
johnpryan Sep 15, 2022
c9f670b
assert that no GoRoute ancestor has a different parentNavigatorKey
johnpryan Sep 15, 2022
9d92bbe
Fix algorithm for checking parent navigator keys
johnpryan Sep 15, 2022
fb36859
Fix valid configuration test
johnpryan Sep 15, 2022
70048b8
Fix incorrect parentNavigatorKey in test
johnpryan Sep 16, 2022
1e37f8f
Fix overlapping parentNavigatorKey assertions
johnpryan Sep 16, 2022
a7426b8
Remove unused variable
johnpryan Sep 16, 2022
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
6 changes: 6 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 4.5.0

- Adds ShellRoute for nested navigation support (flutter/flutter#99126)
- Adds `parentNavigatorKey` to GoRoute, which specifies the Navigator to place that
route's Page onto.

## 4.4.1

- Fix an issue where disabling logging clears the root logger's listeners
Expand Down
42 changes: 42 additions & 0 deletions packages/go_router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,48 @@ methods:
onTap: () => context.go('/page2')
```

## Nested Navigation

The `ShellRoute` route type provides a way to wrap all sub-routes with a UI shell.
Under the hood, GoRouter places a Navigator in the widget tree, which is used
to display matching sub-routes:

```dart
final _router = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) {
return AppScaffold(child: child);
},
routes: <RouteBase>[
GoRoute(
path: '/albums',
builder: (context, state) {
return HomeScreen();
},
routes: <RouteBase>[
/// The details screen to display stacked on the inner Navigator.
GoRoute(
path: 'song/:songId',
builder: (BuildContext context, GoRouterState state) {
return const DetailsScreen(label: 'A');
},
),
],
),
],
),
],
);
```
johnpryan marked this conversation as resolved.
Show resolved Hide resolved

For more details, see the
[ShellRoute](https://pub.dev/documentation/go_router/latest/go_router/ShellRoute-class.html)
API documentation. For a complete
example, see the
[ShellRoute sample](https://github.com/flutter/packages/tree/main/packages/go_router/example/lib/shell_route.dart)
in the example/ directory.

### Still not sure how to proceed?
See [examples](https://github.com/flutter/packages/tree/main/packages/go_router/example) for complete runnable examples or visit [API documentation](https://pub.dev/documentation/go_router/latest/go_router/go_router-library.html)

Expand Down
236 changes: 236 additions & 0 deletions packages/go_router/example/lib/shell_route.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

johnpryan marked this conversation as resolved.
Show resolved Hide resolved
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'shell');

// This scenario demonstrates how to set up nested navigation using ShellRoute,
// which is a pattern where an additional Navigator is placed in the widget tree
// to be used instead of the root navigator. This allows deep-links to display
// pages along with other UI components such as a BottomNavigationBar.
//
// This example demonstrates how to display a route within a ShellRoute and also
// push a screen using a different navigator (such as the root Navigator) by
// providing a `parentNavigatorKey`.

void main() {
runApp(ShellRouteExampleApp());
}

/// An example demonstrating how to use [ShellRoute]
Copy link
Member

@loic-sharma loic-sharma Aug 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried that folks will see this example and think you need to define a root navigator key to use ShellRoute. I wonder if we should have two separate shell route examples:

  1. The simplest shell route example possible
  2. This more advanced shell route example that shows you how to "break out" of the shell route and display on top of the root navigator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM - I'll break it out into two samples

class ShellRouteExampleApp extends StatelessWidget {
/// Creates a [ShellRouteExampleApp]
ShellRouteExampleApp({Key? key}) : super(key: key);

final GoRouter _router = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/a',
routes: <RouteBase>[
/// Application shell
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (BuildContext context, GoRouterState state, Widget child) {
return ScaffoldWithNavBar(child: child);
},
routes: <RouteBase>[
/// The first screen to display in the bottom navigation bar.
GoRoute(
path: '/a',
builder: (BuildContext context, GoRouterState state) {
return const ScreenA();
},
routes: <RouteBase>[
// The details screen to display stacked on the inner Navigator.
// This will cover screen A but not the application shell.
GoRoute(
path: 'details',
builder: (BuildContext context, GoRouterState state) {
return const DetailsScreen(label: 'A');
},
),
],
),

/// Displayed when the second item in the the bottom navigation bar is
/// selected.
GoRoute(
path: '/b',
builder: (BuildContext context, GoRouterState state) {
return const ScreenB();
},
routes: <RouteBase>[
/// Same as "/a/details", but displayed on the root Navigator by
/// specifying [parentNavigatorKey]. This will cover both screen B
/// and the application shell.
GoRoute(
path: 'details',
parentNavigatorKey: _rootNavigatorKey,
builder: (BuildContext context, GoRouterState state) {
return const DetailsScreen(label: 'B');
},
),
],
),
],
),
],
);

@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
routeInformationProvider: _router.routeInformationProvider,
);
}
}

/// Builds the "shell" for the app by building a Scaffold with a
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
class ScaffoldWithNavBar extends StatelessWidget {
/// Constructs an [ScaffoldWithNavBar].
const ScaffoldWithNavBar({
required this.child,
Key? key,
}) : super(key: key);

/// The widget to display in the body of the Scaffold.
/// In this sample, it is a Navigator.
final Widget child;

@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'A Screen',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'B Screen',
),
],
currentIndex: _calculateSelectedIndex(context),
onTap: (int idx) => _onItemTapped(idx, context),
),
);
}

static int _calculateSelectedIndex(BuildContext context) {
final GoRouter route = GoRouter.of(context);
final String location = route.location;
if (location == '/a') {
return 0;
}
if (location == '/b') {
return 1;
}
return 0;
}

void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0:
GoRouter.of(context).go('/a');
break;
case 1:
GoRouter.of(context).go('/b');
break;
}
}
}

/// The first screen in the bottom navigation bar.
class ScreenA extends StatelessWidget {
/// Constructs a [ScreenA] widget.
const ScreenA({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Screen A'),
TextButton(
onPressed: () {
GoRouter.of(context).go('/a/details');
},
child: const Text('View A details'),
),
],
),
),
);
}
}

/// The second screen in the bottom navigation bar.
class ScreenB extends StatelessWidget {
/// Constructs a [ScreenB] widget.
const ScreenB({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Screen B'),
TextButton(
onPressed: () {
GoRouter.of(context).go('/b/details');
},
child: const Text('View B details'),
),
],
),
),
);
}
}

/// The details screen for either the A or B screen.
class DetailsScreen extends StatelessWidget {
/// Constructs a [DetailsScreen].
const DetailsScreen({
required this.label,
Key? key,
}) : super(key: key);

/// The label to display in the center of the screen.
final String label;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Details Screen'),
),
body: Center(
child: Text(
'Details for $label',
style: Theme.of(context).textTheme.headlineMedium,
),
),
);
}
}
3 changes: 2 additions & 1 deletion packages/go_router/lib/go_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
/// deep linking, data-driven routes and more.
library go_router;

export 'src/configuration.dart' show GoRoute, GoRouterState;
export 'src/configuration.dart'
show GoRoute, GoRouterState, RouteBase, ShellRoute;
export 'src/misc/extensions.dart';
export 'src/misc/inherited_router.dart';
export 'src/misc/refresh_stream.dart';
Expand Down
Loading