-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[go_router] Nested stateful navigation with ShellRoute #2650
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
Changes from 1 commit
fa4c542
76b17ae
7612a55
62574bd
850f41a
1c04588
1db6bba
6028c90
5f2e995
461efd9
0315e1a
510ec34
e82647a
e38fa32
f38b9bf
89f1dd7
3a00a54
b0a6264
b729d64
3b3909f
f0b2a1b
eb6d4d3
016be76
4ae26d7
4a9889a
1f3f01a
3b38641
d701ab5
bb6240d
245d47b
bda571b
da1ea0b
59c19e7
723de76
4c0a91d
7556965
ee9bde9
e9a7029
59a6b05
06ab1e9
1b877c8
e542a05
2215a51
b180653
8fdfb82
9240ea4
93bce8e
20dc0c6
376e80f
2b2ff91
5c9fe04
81e1296
59e3b66
8636bf8
1c509f1
42c7b7d
62e7fc1
9a7069a
ae48ede
141fdc1
b6b289f
4d05d99
d4edd47
7b9de47
703815c
9f88928
5ca533d
f5f0ecb
4a2eac3
fc7bd54
a65f9df
816acb2
ee2a845
89b82c5
90b9d62
7392264
c0253f6
2c4afa1
845c052
193a267
37c4969
c562482
38b5772
db22bac
d8d1641
4c4b7b0
f08f548
6f1b047
07ee030
9ffff5c
e9d40ee
0ea48cb
4abcaa9
ce23558
351ceb2
4cb0f1e
e6a4f71
873bf34
5b668f8
565c3cd
5ee1a8f
358551f
f9a2608
87211aa
257a272
08148e5
c18941f
b1ce762
75e43d0
9f54b4e
1226f44
881be86
6bbbd29
fd5412f
a5234a6
b9428cb
ee047c8
ddc71f0
5041bea
01cce04
9315373
89341ef
2c298cd
4c22b53
a7d419f
7285788
9983887
ad43837
3a83007
ec6722c
9bb0da9
a1c7a8f
477b47c
7f25f0c
6f35636
bb5c08f
16c095c
a18a5a5
abbd01b
0f36623
316cf8c
79a0c63
3696448
763b136
2f48831
709ee7a
bb7fd9e
9eaf1af
527a806
86a72e1
57b761f
85fa0b6
d75c5e2
bd1c018
c13cd71
c7b1295
0f842ee
80c396f
04b549a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…tent-navigation # Conflicts: # packages/go_router/CHANGELOG.md # packages/go_router/lib/src/builder.dart # packages/go_router/lib/src/configuration.dart # packages/go_router/lib/src/delegate.dart # packages/go_router/lib/src/matching.dart # packages/go_router/lib/src/redirection.dart # packages/go_router/lib/src/router.dart # packages/go_router/test/builder_test.dart
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; | |||||||||||||||||
| import 'configuration.dart'; | ||||||||||||||||||
| import 'logging.dart'; | ||||||||||||||||||
| import 'matching.dart'; | ||||||||||||||||||
| import 'misc/errors.dart'; | ||||||||||||||||||
| import 'path_utils.dart'; | ||||||||||||||||||
| import 'typedefs.dart'; | ||||||||||||||||||
| export 'route.dart'; | ||||||||||||||||||
|
|
@@ -36,95 +37,82 @@ class RouteConfiguration { | |||||||||||||||||
| if (route is GoRoute) { | ||||||||||||||||||
| if (isTopLevel) { | ||||||||||||||||||
| assert(route.path.startsWith('/'), | ||||||||||||||||||
| 'top-level path must start with "/": ${route.path}'); | ||||||||||||||||||
| } else if (route is ShellRouteBase) { | ||||||||||||||||||
| for (final RouteBase route in routes) { | ||||||||||||||||||
| if (route is GoRoute) { | ||||||||||||||||||
| assert(route.path.startsWith('/'), | ||||||||||||||||||
| 'top-level path must start with "/": ${route.path}'); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| 'top-level path must start with "/": $route'); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| assert(!route.path.startsWith('/') && !route.path.endsWith('/'), | ||||||||||||||||||
| 'sub-route path may not start or end with /: $route'); | ||||||||||||||||||
| } | ||||||||||||||||||
| subRouteIsTopLevel = false; | ||||||||||||||||||
| } else if (route is ShellRoute) { | ||||||||||||||||||
| } else if (route is ShellRouteBase) { | ||||||||||||||||||
| subRouteIsTopLevel = isTopLevel; | ||||||||||||||||||
| } | ||||||||||||||||||
| _debugCheckPath(route.routes, subRouteIsTopLevel); | ||||||||||||||||||
| } | ||||||||||||||||||
| return true; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Check that each parentNavigatorKey refers to either a ShellRoute's | ||||||||||||||||||
| // navigatorKey or the root navigator key. | ||||||||||||||||||
| void checkParentNavigatorKeys( | ||||||||||||||||||
| List<RouteBase> routes, | ||||||||||||||||||
| List<GlobalKey<NavigatorState>> allowedKeys, | ||||||||||||||||||
| Set<GlobalKey<NavigatorState>> allNavigatorKeys) { | ||||||||||||||||||
| for (final RouteBase route in routes) { | ||||||||||||||||||
| if (route is GoRoute) { | ||||||||||||||||||
| final GlobalKey<NavigatorState>? parentKey = | ||||||||||||||||||
| route.parentNavigatorKey; | ||||||||||||||||||
| if (parentKey != null) { | ||||||||||||||||||
| // Verify that the root navigator or a ShellRoute ancestor has a | ||||||||||||||||||
| // matching navigator key. | ||||||||||||||||||
| assert( | ||||||||||||||||||
| allowedKeys.contains(parentKey), | ||||||||||||||||||
| 'parentNavigatorKey $parentKey must refer to' | ||||||||||||||||||
| " an ancestor ShellRoute's navigatorKey or GoRouter's" | ||||||||||||||||||
| ' navigatorKey'); | ||||||||||||||||||
|
|
||||||||||||||||||
| allNavigatorKeys.add(parentKey); | ||||||||||||||||||
| checkParentNavigatorKeys( | ||||||||||||||||||
| route.routes, | ||||||||||||||||||
| <GlobalKey<NavigatorState>>[ | ||||||||||||||||||
| // Once a parentNavigatorKey is used, only that navigator key | ||||||||||||||||||
| // or keys above it can be used. | ||||||||||||||||||
| ...allowedKeys.sublist(0, allowedKeys.indexOf(parentKey) + 1), | ||||||||||||||||||
| ], | ||||||||||||||||||
| allNavigatorKeys, | ||||||||||||||||||
| ); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| checkParentNavigatorKeys( | ||||||||||||||||||
| route.routes, | ||||||||||||||||||
| <GlobalKey<NavigatorState>>[ | ||||||||||||||||||
| ...allowedKeys, | ||||||||||||||||||
| ], | ||||||||||||||||||
| allNavigatorKeys, | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| } else if (route is ShellRoute && route.navigatorKey != null) { | ||||||||||||||||||
| allNavigatorKeys.add(route.navigatorKey); | ||||||||||||||||||
| checkParentNavigatorKeys( | ||||||||||||||||||
| route.routes, | ||||||||||||||||||
| <GlobalKey<NavigatorState>>[ | ||||||||||||||||||
| ...allowedKeys..add(route.navigatorKey) | ||||||||||||||||||
| ], | ||||||||||||||||||
| allNavigatorKeys, | ||||||||||||||||||
| ); | ||||||||||||||||||
| } else if (route is StatefulShellRoute) { | ||||||||||||||||||
| checkParentNavigatorKeys( | ||||||||||||||||||
| route.routes, allowedKeys, allNavigatorKeys); | ||||||||||||||||||
| } | ||||||||||||||||||
| // Check that each parentNavigatorKey refers to either a ShellRoute's | ||||||||||||||||||
| // navigatorKey or the root navigator key. | ||||||||||||||||||
| static bool _debugCheckParentNavigatorKeys( | ||||||||||||||||||
| List<RouteBase> routes, List<GlobalKey<NavigatorState>> allowedKeys) { | ||||||||||||||||||
| for (final RouteBase route in routes) { | ||||||||||||||||||
| if (route is GoRoute) { | ||||||||||||||||||
| final GlobalKey<NavigatorState>? parentKey = route.parentNavigatorKey; | ||||||||||||||||||
| if (parentKey != null) { | ||||||||||||||||||
| // Verify that the root navigator or a ShellRoute ancestor has a | ||||||||||||||||||
| // matching navigator key. | ||||||||||||||||||
| assert( | ||||||||||||||||||
| allowedKeys.contains(parentKey), | ||||||||||||||||||
| 'parentNavigatorKey $parentKey must refer to' | ||||||||||||||||||
| " an ancestor ShellRoute's navigatorKey or GoRouter's" | ||||||||||||||||||
| ' navigatorKey'); | ||||||||||||||||||
|
|
||||||||||||||||||
| _debugCheckParentNavigatorKeys( | ||||||||||||||||||
| route.routes, | ||||||||||||||||||
| <GlobalKey<NavigatorState>>[ | ||||||||||||||||||
| // Once a parentNavigatorKey is used, only that navigator key | ||||||||||||||||||
| // or keys above it can be used. | ||||||||||||||||||
| ...allowedKeys.sublist(0, allowedKeys.indexOf(parentKey) + 1), | ||||||||||||||||||
| ], | ||||||||||||||||||
| ); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| _debugCheckParentNavigatorKeys( | ||||||||||||||||||
| route.routes, | ||||||||||||||||||
| <GlobalKey<NavigatorState>>[ | ||||||||||||||||||
| ...allowedKeys, | ||||||||||||||||||
| ], | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| } else if (route is ShellRoute && route.navigatorKey != null) { | ||||||||||||||||||
| _debugCheckParentNavigatorKeys( | ||||||||||||||||||
| route.routes, | ||||||||||||||||||
| <GlobalKey<NavigatorState>>[...allowedKeys..add(route.navigatorKey)], | ||||||||||||||||||
| ); | ||||||||||||||||||
| } else if (route is StatefulShellRoute) { | ||||||||||||||||||
| _debugCheckParentNavigatorKeys(route.routes, allowedKeys); | ||||||||||||||||||
|
||||||||||||||||||
| _debugCheckParentNavigatorKeys(route.routes, allowedKeys); | |
| _debugCheckParentNavigatorKeys( | |
| route.routes, | |
| <GlobalKey<NavigatorState>>[ | |
| ...allowedKeys, | |
| ...route.branches.map((b) => b.navigatorKey), | |
| ], | |
| ); |
If needed, here is an example app crashing with current code and fixed with the above suggested change.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// This example demonstrates how to setup nested navigation using a
// BottomNavigationBar, where each tab uses its own persistent navigator, i.e.
// navigation state is maintained separately for each tab. This setup also
// enables deep linking into nested pages.
//
// This example demonstrates how to display routes within a StatefulShellRoute,
// that are places on separate navigators. The example also demonstrates how
// state is maintained when switching between different tabs (and thus branches
// and Navigators).
void main() {
runApp(NestedTabNavigationExampleApp());
}
final GlobalKey<NavigatorState> _bShellNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'bShell');
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'shell');
/// The details screen for either the A or B screen.
class DetailsScreen extends StatefulWidget {
/// The label to display in the center of the screen.
final String label;
/// Optional param
final String? param;
/// Optional extra object
final Object? extra;
/// Wrap in scaffold
final bool withScaffold;
/// Constructs a [DetailsScreen].
const DetailsScreen({
required this.label,
this.param,
this.extra,
this.withScaffold = true,
Key? key,
}) : super(key: key);
@override
State<StatefulWidget> createState() => DetailsScreenState();
}
/// The state for DetailsScreen
class DetailsScreenState extends State<DetailsScreen> {
int _counter = 0;
@override
Widget build(BuildContext context) {
if (widget.withScaffold) {
return Scaffold(
appBar: AppBar(
title: Text('Details Screen - ${widget.label}'),
),
body: _build(context),
);
} else {
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: _build(context),
);
}
}
Widget _build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Details for ${widget.label} - Counter: $_counter',
style: Theme.of(context).textTheme.titleLarge),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: const Text('Increment counter'),
),
const Padding(padding: EdgeInsets.all(8)),
if (widget.param != null)
Text('Parameter: ${widget.param!}',
style: Theme.of(context).textTheme.titleMedium),
const Padding(padding: EdgeInsets.all(8)),
if (widget.extra != null)
Text('Extra: ${widget.extra!}',
style: Theme.of(context).textTheme.titleMedium),
if (!widget.withScaffold) ...<Widget>[
const Padding(padding: EdgeInsets.all(16)),
TextButton(
onPressed: () {
GoRouter.of(context).pop();
},
child: const Text('< Back',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
),
]
],
),
);
}
}
/// Widget for a modal screen.
class ModalScreen extends StatelessWidget {
/// Creates a ModalScreen
const ModalScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Modal'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Modal screen', style: Theme.of(context).textTheme.titleLarge),
const Padding(padding: EdgeInsets.all(8)),
ElevatedButton(
onPressed: () {
GoRouter.of(context).go('/a');
},
child: const Text('Go to initial section'),
),
],
),
),
);
}
}
/// An example demonstrating how to use nested navigators
class NestedTabNavigationExampleApp extends StatelessWidget {
final GoRouter _router = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/a',
routes: <RouteBase>[
GoRoute(
path: '/modal',
parentNavigatorKey: _rootNavigatorKey,
builder: (BuildContext context, GoRouterState state) =>
const ModalScreen(),
),
StatefulShellRoute(
branches: <StatefulShellBranch>[
/// The route branch for the first tab of the bottom navigation bar.
StatefulShellBranch(
/// To enable preloading of the initial locations of branches, pass
/// true for the parameter preload.
// preload: true,
routes: <RouteBase>[
GoRoute(
/// The screen to display as the root in the first tab of the
/// bottom navigation bar.
path: '/a',
builder: (BuildContext context, GoRouterState state) =>
const RootScreen(label: 'A', detailsPath: '/a/details'),
routes: <RouteBase>[
/// The details screen to display stacked on navigator of the
/// first tab. This will cover screen A but not the application
/// shell (bottom navigation bar).
GoRoute(
path: 'details',
builder: (BuildContext context, GoRouterState state) =>
DetailsScreen(label: 'A', extra: state.extra),
),
],
),
],
),
/// The route branch for the third tab of the bottom navigation bar.
StatefulShellBranch(
/// StatefulShellBranch will automatically use the first descendant
/// GoRoute as the initial location of the branch. If another route
/// is desired, specify the location of it using the defaultLocation
/// parameter.
// defaultLocation: '/b/2',
navigatorKey: _shellNavigatorKey,
routes: <RouteBase>[
GoRoute(
path: '/b',
builder: (BuildContext context, GoRouterState state) =>
const RootScreen(label: 'B Root', detailsPath: '/b/1'),
redirect: (context, state) {
print('Redirecting from ${state.location}');
if (state.location == '/b') {
return '/b/1';
}
return null;
},
),
StatefulShellRoute(
/// This bottom tab uses a nested shell, wrapping sub routes in a
/// top TabBar.
branches: <StatefulShellBranch>[
StatefulShellBranch(
routes: <GoRoute>[
GoRoute(
path: '/b/1',
builder: (BuildContext context, GoRouterState state) =>
const TabScreen(
label: 'B1', detailsPath: '/b/1/details'),
routes: <RouteBase>[
GoRoute(
path: 'details',
builder:
(BuildContext context, GoRouterState state) =>
DetailsScreen(
label: 'B1',
extra: state.extra,
withScaffold: false,
),
),
],
),
],
),
StatefulShellBranch(
navigatorKey: _bShellNavigatorKey,
routes: <GoRoute>[
GoRoute(
path: '/b/2',
builder: (BuildContext context, GoRouterState state) =>
const TabScreen(
label: 'B2', detailsPath: '/b/2/details'),
routes: <RouteBase>[
GoRoute(
path: 'details',
parentNavigatorKey: _shellNavigatorKey,
builder:
(BuildContext context, GoRouterState state) =>
DetailsScreen(
label: 'B2',
extra: state.extra,
withScaffold: false,
),
),
],
),
],
),
],
builder: (StatefulShellBuilder shellBuilder) {
/// For this nested StatefulShellRoute, a custom container
/// (TabBarView) is used for the branch navigators, and thus
/// ignoring the default navigator container passed to the
/// builder. Instead, the branch navigators are passed
/// directly to the TabbedRootScreen, using the children
/// field of ShellNavigatorContainer. See TabbedRootScreen
/// for more details on how the children are used in the
/// TabBarView.
return shellBuilder.buildShell(
(BuildContext context, GoRouterState state,
ShellNavigatorContainer child) {
print('Location: ${state.location}');
return TabbedRootScreen(
children: child.children,
);
},
);
},
),
],
),
/// The route branch for the second tab of the bottom navigation bar.
StatefulShellBranch(
/// It's not necessary to provide a navigatorKey if it isn't also
/// needed elsewhere. If not provided, a default key will be used.
routes: <RouteBase>[
GoRoute(
/// The screen to display as the root in the second tab of the
/// bottom navigation bar.
path: '/c',
builder: (BuildContext context, GoRouterState state) =>
const RootScreen(
label: 'C',
detailsPath: '/c/details/1',
secondDetailsPath: '/c/details/2',
),
routes: <RouteBase>[
GoRoute(
path: 'details/:param',
builder: (BuildContext context, GoRouterState state) =>
DetailsScreen(
label: 'C',
param: state.params['param'],
extra: state.extra,
),
),
],
),
],
),
],
builder: (StatefulShellBuilder shellBuilder) {
/// This builder implementation uses the default navigator container
/// (ShellNavigatorContainer) to host the branch navigators. This is
/// the simplest way to use StatefulShellRoute, when no separate
/// customization is needed for the branch Widgets (Navigators).
return shellBuilder.buildShell((BuildContext context,
GoRouterState state, ShellNavigatorContainer child) =>
ScaffoldWithNavBar(body: child));
},
/// If it's necessary to customize the Page for StatefulShellRoute,
/// provide a pageBuilder function instead of the builder, for example:
// pageBuilder: (StatefulShellBuilder shellBuilder) {
// final Widget statefulShell = shellBuilder.buildShell(
// (BuildContext context, GoRouterState state,
// ShellNavigatorContainer child) =>
// ScaffoldWithNavBar(body: child));
// return NoTransitionPage<dynamic>(child: statefulShell);
// },
),
],
);
/// Creates a NestedTabNavigationExampleApp
NestedTabNavigationExampleApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerConfig: _router,
);
}
}
/// Widget for the root/initial pages in the bottom navigation bar.
class RootScreen extends StatelessWidget {
/// The label
final String label;
/// The path to the detail page
final String detailsPath;
/// The path to another detail page
final String? secondDetailsPath;
/// Creates a RootScreen
const RootScreen(
{required this.label,
required this.detailsPath,
this.secondDetailsPath,
Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Tab root - $label'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Screen $label',
style: Theme.of(context).textTheme.titleLarge),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
GoRouter.of(context).go(detailsPath, extra: '$label-XYZ');
},
child: const Text('View details'),
),
const Padding(padding: EdgeInsets.all(4)),
if (secondDetailsPath != null)
TextButton(
onPressed: () {
GoRouter.of(context).go(secondDetailsPath!);
},
child: const Text('View more details'),
),
const Padding(padding: EdgeInsets.all(8)),
ElevatedButton(
onPressed: () {
// GoRouter.of(context).push('/modal');
GoRouter.of(context).go('/c/2/details');
},
child: const Text('Show modal screen on root navigator'),
),
const Padding(padding: EdgeInsets.all(4)),
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: _bottomSheet);
},
child: const Text('Show bottom sheet on root navigator'),
),
],
),
),
);
}
Widget _bottomSheet(BuildContext context) {
return Container(
height: 200,
color: Colors.amber,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Modal BottomSheet'),
ElevatedButton(
child: const Text('Close BottomSheet'),
onPressed: () => Navigator.pop(context),
),
],
),
),
);
}
}
/// 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 {
/// Body, i.e. the index stack
final Widget body;
/// Constructs an [ScaffoldWithNavBar].
const ScaffoldWithNavBar({
required this.body,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
@override
Widget build(BuildContext context) {
final StatefulShellRouteState shellState =
StatefulShellRouteState.of(context);
return Scaffold(
body: body,
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'),
],
currentIndex: shellState.currentIndex,
onTap: (int tappedIndex) => shellState.goBranch(index: tappedIndex),
),
);
}
}
/// Builds a nested shell using a [TabBar] and [TabBarView].
class TabbedRootScreen extends StatelessWidget {
/// The children (Navigators) to display in the [TabBarView].
final List<Widget> children;
/// Constructs a TabbedRootScreen
const TabbedRootScreen({required this.children, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final StatefulShellRouteState shellState =
StatefulShellRouteState.of(context);
print('currIndex: ${shellState.currentIndex}');
final List<Tab> tabs =
children.mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')).toList();
return DefaultTabController(
key: ValueKey(shellState.currentIndex),
length: children.length,
initialIndex: shellState.currentIndex,
child: Scaffold(
appBar: AppBar(
title: const Text('Tab root'),
bottom: TabBar(
tabs: tabs,
onTap: (int tappedIndex) => _onTabTap(context, tappedIndex),
)),
body: TabBarView(
children: children,
),
),
);
}
void _onTabTap(BuildContext context, int index) {
StatefulShellRouteState.of(context).goBranch(index: index);
}
}
/// Widget for the pages in the top tab bar.
class TabScreen extends StatelessWidget {
/// The label
final String label;
/// The path to the detail page
final String? detailsPath;
/// Creates a RootScreen
const TabScreen({required this.label, this.detailsPath, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
/// If preloading is enabled on the top StatefulShellRoute, this will be
/// printed directly after the app has been started, but only for the route
/// that is the initial location ('/c/1')
debugPrint('Building TabScreen - $label');
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Screen $label', style: Theme.of(context).textTheme.titleLarge),
const Padding(padding: EdgeInsets.all(4)),
if (detailsPath != null)
TextButton(
onPressed: () {
GoRouter.of(context).go(detailsPath!);
},
child: const Text('View details'),
),
],
),
);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes they should, nice catch, thanks! This code used to be there, but was removed as part of the attempt to add dynamic branches. That change was later reverted, but I must have missed to properly revert the change in the assertion code. Will fix this and add a unit test for this also.
Uh oh!
There was an error while loading. Please reload this page.