Skip to content

Commit e497f6f

Browse files
authored
Support stateful bottom navigation (introduced in GoRouter 7.1) (bizz84#136)
* Support stateful bottom navigation (introduced in GoRouter 7.1) * No longer passing state.pageKey explicitly
1 parent 94046d3 commit e497f6f

File tree

2 files changed

+112
-120
lines changed

2 files changed

+112
-120
lines changed

lib/src/routing/app_router.dart

Lines changed: 94 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ part 'app_router.g.dart';
2020

2121
// private navigators
2222
final _rootNavigatorKey = GlobalKey<NavigatorState>();
23-
final _shellNavigatorKey = GlobalKey<NavigatorState>();
23+
final _jobsNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'jobs');
24+
final _entriesNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'entries');
25+
final _accountNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'account');
2426

2527
enum AppRoute {
2628
onboarding,
@@ -73,120 +75,126 @@ GoRouter goRouter(GoRouterRef ref) {
7375
GoRoute(
7476
path: '/onboarding',
7577
name: AppRoute.onboarding.name,
76-
pageBuilder: (context, state) => NoTransitionPage(
77-
key: state.pageKey,
78-
child: const OnboardingScreen(),
78+
pageBuilder: (context, state) => const NoTransitionPage(
79+
child: OnboardingScreen(),
7980
),
8081
),
8182
GoRoute(
8283
path: '/signIn',
8384
name: AppRoute.signIn.name,
84-
pageBuilder: (context, state) => NoTransitionPage(
85-
key: state.pageKey,
86-
child: const CustomSignInScreen(),
85+
pageBuilder: (context, state) => const NoTransitionPage(
86+
child: CustomSignInScreen(),
8787
),
8888
),
89-
ShellRoute(
90-
navigatorKey: _shellNavigatorKey,
91-
builder: (context, state, child) {
92-
return ScaffoldWithBottomNavBar(child: child);
89+
// Stateful navigation based on:
90+
// https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
91+
StatefulShellRoute.indexedStack(
92+
builder: (context, state, navigationShell) {
93+
return ScaffoldWithBottomNavBar(navigationShell: navigationShell);
9394
},
94-
routes: [
95-
GoRoute(
96-
path: '/jobs',
97-
name: AppRoute.jobs.name,
98-
pageBuilder: (context, state) => NoTransitionPage(
99-
key: state.pageKey,
100-
child: const JobsScreen(),
101-
),
95+
branches: [
96+
StatefulShellBranch(
97+
navigatorKey: _jobsNavigatorKey,
10298
routes: [
10399
GoRoute(
104-
path: 'add',
105-
name: AppRoute.addJob.name,
106-
parentNavigatorKey: _rootNavigatorKey,
107-
pageBuilder: (context, state) {
108-
return MaterialPage(
109-
key: state.pageKey,
110-
fullscreenDialog: true,
111-
child: const EditJobScreen(),
112-
);
113-
},
114-
),
115-
GoRoute(
116-
path: ':id',
117-
name: AppRoute.job.name,
118-
pageBuilder: (context, state) {
119-
final id = state.pathParameters['id']!;
120-
return MaterialPage(
121-
key: state.pageKey,
122-
child: JobEntriesScreen(jobId: id),
123-
);
124-
},
100+
path: '/jobs',
101+
name: AppRoute.jobs.name,
102+
pageBuilder: (context, state) => const NoTransitionPage(
103+
child: JobsScreen(),
104+
),
125105
routes: [
126106
GoRoute(
127-
path: 'entries/add',
128-
name: AppRoute.addEntry.name,
107+
path: 'add',
108+
name: AppRoute.addJob.name,
129109
parentNavigatorKey: _rootNavigatorKey,
130110
pageBuilder: (context, state) {
131-
final jobId = state.pathParameters['id']!;
132-
return MaterialPage(
133-
key: state.pageKey,
111+
return const MaterialPage(
134112
fullscreenDialog: true,
135-
child: EntryScreen(
136-
jobId: jobId,
137-
),
138-
);
139-
},
140-
),
141-
GoRoute(
142-
path: 'entries/:eid',
143-
name: AppRoute.entry.name,
144-
pageBuilder: (context, state) {
145-
final jobId = state.pathParameters['id']!;
146-
final entryId = state.pathParameters['eid']!;
147-
final entry = state.extra as Entry?;
148-
return MaterialPage(
149-
key: state.pageKey,
150-
child: EntryScreen(
151-
jobId: jobId,
152-
entryId: entryId,
153-
entry: entry,
154-
),
113+
child: EditJobScreen(),
155114
);
156115
},
157116
),
158117
GoRoute(
159-
path: 'edit',
160-
name: AppRoute.editJob.name,
118+
path: ':id',
119+
name: AppRoute.job.name,
161120
pageBuilder: (context, state) {
162-
final jobId = state.pathParameters['id'];
163-
final job = state.extra as Job?;
121+
final id = state.pathParameters['id']!;
164122
return MaterialPage(
165-
key: state.pageKey,
166-
fullscreenDialog: true,
167-
child: EditJobScreen(jobId: jobId, job: job),
123+
child: JobEntriesScreen(jobId: id),
168124
);
169125
},
126+
routes: [
127+
GoRoute(
128+
path: 'entries/add',
129+
name: AppRoute.addEntry.name,
130+
parentNavigatorKey: _rootNavigatorKey,
131+
pageBuilder: (context, state) {
132+
final jobId = state.pathParameters['id']!;
133+
return MaterialPage(
134+
fullscreenDialog: true,
135+
child: EntryScreen(
136+
jobId: jobId,
137+
),
138+
);
139+
},
140+
),
141+
GoRoute(
142+
path: 'entries/:eid',
143+
name: AppRoute.entry.name,
144+
pageBuilder: (context, state) {
145+
final jobId = state.pathParameters['id']!;
146+
final entryId = state.pathParameters['eid']!;
147+
final entry = state.extra as Entry?;
148+
return MaterialPage(
149+
child: EntryScreen(
150+
jobId: jobId,
151+
entryId: entryId,
152+
entry: entry,
153+
),
154+
);
155+
},
156+
),
157+
GoRoute(
158+
path: 'edit',
159+
name: AppRoute.editJob.name,
160+
pageBuilder: (context, state) {
161+
final jobId = state.pathParameters['id'];
162+
final job = state.extra as Job?;
163+
return MaterialPage(
164+
fullscreenDialog: true,
165+
child: EditJobScreen(jobId: jobId, job: job),
166+
);
167+
},
168+
),
169+
],
170170
),
171171
],
172172
),
173173
],
174174
),
175-
GoRoute(
176-
path: '/entries',
177-
name: AppRoute.entries.name,
178-
pageBuilder: (context, state) => NoTransitionPage(
179-
key: state.pageKey,
180-
child: const EntriesScreen(),
181-
),
175+
StatefulShellBranch(
176+
navigatorKey: _entriesNavigatorKey,
177+
routes: [
178+
GoRoute(
179+
path: '/entries',
180+
name: AppRoute.entries.name,
181+
pageBuilder: (context, state) => const NoTransitionPage(
182+
child: EntriesScreen(),
183+
),
184+
),
185+
],
182186
),
183-
GoRoute(
184-
path: '/account',
185-
name: AppRoute.profile.name,
186-
pageBuilder: (context, state) => NoTransitionPage(
187-
key: state.pageKey,
188-
child: const CustomProfileScreen(),
189-
),
187+
StatefulShellBranch(
188+
navigatorKey: _accountNavigatorKey,
189+
routes: [
190+
GoRoute(
191+
path: '/account',
192+
name: AppRoute.profile.name,
193+
pageBuilder: (context, state) => const NoTransitionPage(
194+
child: CustomProfileScreen(),
195+
),
196+
),
197+
],
190198
),
191199
],
192200
),

lib/src/routing/scaffold_with_bottom_nav_bar.dart

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,34 @@
11
import 'package:flutter/material.dart';
22
import 'package:go_router/go_router.dart';
33
import 'package:starter_architecture_flutter_firebase/src/localization/string_hardcoded.dart';
4-
import 'package:starter_architecture_flutter_firebase/src/routing/app_router.dart';
54

6-
// This is a temporary implementation
7-
// TODO: Implement a better solution once this PR is merged:
8-
// https://github.com/flutter/packages/pull/2650
9-
class ScaffoldWithBottomNavBar extends StatefulWidget {
10-
const ScaffoldWithBottomNavBar({Key? key, required this.child})
11-
: super(key: key);
12-
final Widget child;
13-
14-
@override
15-
State<ScaffoldWithBottomNavBar> createState() =>
16-
_ScaffoldWithBottomNavBarState();
17-
}
18-
19-
class _ScaffoldWithBottomNavBarState extends State<ScaffoldWithBottomNavBar> {
20-
// used for the currentIndex argument of BottomNavigationBar
21-
int _selectedIndex = 0;
5+
// Stateful navigation based on:
6+
// https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
7+
class ScaffoldWithBottomNavBar extends StatelessWidget {
8+
const ScaffoldWithBottomNavBar({
9+
Key? key,
10+
required this.navigationShell,
11+
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithBottomNavBar'));
12+
final StatefulNavigationShell navigationShell;
2213

2314
void _tap(BuildContext context, int index) {
24-
if (index == _selectedIndex) {
25-
// If the tab hasn't changed, do nothing
26-
return;
27-
}
28-
setState(() => _selectedIndex = index);
29-
if (index == 0) {
30-
// Note: this won't remember the previous state of the route
31-
// More info here:
32-
// https://github.com/flutter/flutter/issues/99124
33-
context.goNamed(AppRoute.jobs.name);
34-
} else if (index == 1) {
35-
context.goNamed(AppRoute.entries.name);
36-
} else if (index == 2) {
37-
context.goNamed(AppRoute.profile.name);
38-
}
15+
navigationShell.goBranch(
16+
index,
17+
// A common pattern when using bottom navigation bars is to support
18+
// navigating to the initial location when tapping the item that is
19+
// already active. This example demonstrates how to support this behavior,
20+
// using the initialLocation parameter of goBranch.
21+
initialLocation: index == navigationShell.currentIndex,
22+
);
3923
}
4024

4125
@override
4226
Widget build(BuildContext context) {
4327
return Scaffold(
44-
body: widget.child,
28+
body: navigationShell,
4529
bottomNavigationBar: BottomNavigationBar(
4630
type: BottomNavigationBarType.fixed,
47-
currentIndex: _selectedIndex,
31+
currentIndex: navigationShell.currentIndex,
4832
items: [
4933
// products
5034
BottomNavigationBarItem(

0 commit comments

Comments
 (0)