I’m working on a Flutter app where I’m using GoRouter for navigation with a bottom navigation bar to switch between different sections. Each section has its own nested navigation stack.
I want to automatically display a back button in the AppBar when the user is not on the root route of a section. However, my current implementation is not working as expected. Specifically, the following line does not correctly determine if there is a route to pop in the nested navigation stack:
final canPop = navigatorKey.currentState?.canPop() ?? false;
Here is a simplified version of my code:
app.dart
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hanazono_app/src/authentication/auth_gate.dart';
import 'package:hanazono_app/views/collection_screen.dart';
import 'package:hanazono_app/views/conversation_screen.dart';
import 'package:hanazono_app/views/home_screen.dart';
import 'package:hanazono_app/views/new_specimen_screen.dart';
import 'package:hanazono_app/views/scaffold_with_navbar.dart';
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _homeNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'home');
final GlobalKey<NavigatorState> _collectionNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'collection');
final GlobalKey<NavigatorState> _newSpecimenNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'newSpecimen');
final GlobalKey<NavigatorState> _conversationNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'conversation');
class MyApp extends ConsumerStatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends ConsumerState<MyApp> {
late final GoRouter _router;
@override
void initState() {
super.initState();
_router = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/',
routes: [
StatefulShellRoute.indexedStack(
parentNavigatorKey: _rootNavigatorKey,
builder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) {
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
navigatorKey: _homeNavigatorKey,
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
],
),
StatefulShellBranch(
navigatorKey: _collectionNavigatorKey,
routes: [
GoRoute(
path: '/collection',
parentNavigatorKey: _collectionNavigatorKey,
builder: (context, state) => const CollectionScreen(),
routes: [
GoRoute(
path: 'newSpecimen',
builder: (context, state) => const NewSpecimenScreen(),
),
]),
],
),
StatefulShellBranch(
navigatorKey: _newSpecimenNavigatorKey,
routes: [
GoRoute(
path: '/newSpecimen0',
builder: (context, state) => const NewSpecimenScreen(),
),
],
),
StatefulShellBranch(
navigatorKey: _conversationNavigatorKey,
routes: [
GoRoute(
path: '/conversation',
builder: (context, state) {
final modelId = state.uri.queryParameters['modelId'] ??
'exampleModelId';
final modelType =
state.uri.queryParameters['modelType'] ?? 'Taxon';
return ConversationScreen(
modelId: modelId, modelType: modelType);
},
),
],
),
],
),
GoRoute(
path: '/auth',
builder: (context, state) => const AuthGate(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
);
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
scaffold_with_navbar.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers.dart';
class ScaffoldWithNavBar extends ConsumerWidget {
final StatefulNavigationShell navigationShell;
const ScaffoldWithNavBar({
required this.navigationShell,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
@override
Widget build(BuildContext context, WidgetRef ref) {
final GoRouter router = GoRouter.of(context);
final currentLocation = router.routerDelegate.currentConfiguration.fullPath;
Logger.root.info('Current path: $currentLocation');
int currentIndex = navigationShell.currentIndex;
Logger.root.info('Current index: $currentIndex');
final user = ref.watch(authProvider).maybeWhen(
data: (user) => user,
orElse: () => null,
);
final navigatorKey = navigationShell.route.branches[currentIndex].navigatorKey;
final canPop = navigatorKey.currentState?.canPop() ?? false;
Logger.root.info('Should display back button: $canPop');
// Log the state of the current tab's navigation stack
if (navigatorKey.currentState != null) {
Logger.root.info(
'Current tab's navigation stack: ${navigatorKey.currentState!.widget.toString()}');
}
return Scaffold(
appBar: AppBar(
leading: canPop
? IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Logger.root.info('Back button pressed');
navigatorKey.currentState?.pop();
},
)
: null,
title: const Text('My App'),
actions: [
if (user != null && !user.isAnonymous)
IconButton(
icon: Icon(Icons.person),
onPressed: () {
context.go('/profile');
},
)
else
TextButton(
onPressed: () {
context.go('/auth');
},
child: Text('Login'),
),
],
),
body: navigationShell,
bottomNavigationBar: NavigationBar(
backgroundColor: Colors.white,
selectedIndex: currentIndex,
destinations: const [
NavigationDestination(
icon: Icon(Icons.home),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.local_florist),
label: 'Collection',
),
NavigationDestination(
icon: Icon(Icons.add),
label: 'New Specimen',
),
NavigationDestination(
icon: Icon(Icons.chat),
label: 'Conversation',
),
],
onDestinationSelected: (index) {
Logger.root.info('Bottom navigation item tapped: $index');
navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
);
},
),
);
}
}
Collection Screen
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers.dart';
class CollectionScreen extends ConsumerWidget {
const CollectionScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentTabIndex = ref.watch(currentTabProvider);
final navigatorKey =
ref.read(navigationShellProvider.notifier).state[currentTabIndex];
return Scaffold(
body: Center(
child: Text(
'This is the collection screen',
style: Theme.of(context).textTheme.headlineLarge,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.push('/collection/newSpecimen');
},
child: Icon(Icons.add),
),
);
}
}
Issue:
When navigating within a tab, the currentLocation only shows the base path of the tab, not the specific nested route (e.g., /collection instead of /collection/newSpecimen). Consequently, the back button logic does not work correctly.
Goal:
Properly manage the nested navigation stack for each tab so that the back button is displayed when not on the root route of the tab.
Question:
How can I correctly check the nested navigation stack for each tab to determine if the back button should be displayed?
Thank you for your help!