Reputation: 1
I want to create 3 pages using NavigationBar, body using TabBarView with AutomationKeepAliveClientMixin (to keep page state). The subpages of these 3 pages do not have NavigationBar.
My goRouter part looks like this:
GoRoute(
name: "home",
path: "/",
builder: (context, state) => const HomePage(),
),
GoRoute(
name: "settings",
path: "/settings",
builder: (context, state) => const SettingsPage(),
routes: [
GoRoute(
name: "settings-edit",
path: "edit",
builder: (context, state) => const SettingsEditPage(),
),
]
),
GoRoute(
name: "calendar",
path: "/calendar",
builder: (context, state) => const CalendarPage(),
)
I followed the instructions from https://docs.page/csells/go_router/nested-navigation. But I can't understand why it doesn't work properly, maybe because I created many different GoRouters, but in the tutorial there is only 1.
I also followed the 2 instructions at https://stackoverflow.com/questions/71011598/how-to-work-with-navigationbar-in-go-router-flutter.
1.with ShellRouter and using GlobalKey<NavigatorState>() it works but cannot be used with TabBarView and also does not retain page state.
2.With StatefulShellRoute it seems more modern, but I also don't know how to use it in TabBarView
Finally I was able to temporarily get it working by leaving the TabBarView in the ShellRoute, and using _rootNavigatorKey to bring the child pages out.
// go router
GoRoute(
navigatorKey: _rootNavigatorKey,
ShellRoute(
builder: (context, state, child) => MainPage(location: state.uri.toString()),
routes: [
GoRoute(
name: "home",
path: "/",
builder: (context, state) => const SizedBox(),
),
GoRoute(
name: "settings",
path: "/settings",
builder: (context, state) => throw "",
routes: [
GoRoute(
name: "settings-edit",
path: "edit",
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const SettingsEditPage(),
),
]
),
GoRoute(
name: "calendar",
path: "/calendar",
builder: (context, state) => throw "",
),
]
)
)
// main Page
class MainPage extends ConsumerStatefulWidget {
final String location;
const MainPage({required this.location, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _MainPageState();
}
class _MainPageState extends ConsumerState<MainPage> with TickerProviderStateMixin {
late final TabController _tabController;
int currentPageIndex = 0;
late List<Map<String, dynamic>> menu = [
{
"location": "/",
"page": const HomePage(),
},
....
];
void changePage({String? location}) {
int index = menu.indexWhere((e) => e['location'] == (location ?? widget.location));
if (index < 0) {
index = 0;
}
_tabController.animateTo(index);
}
@override
void initState() {
super.initState();
_tabController = TabController(length: menu.length, vsync: this);
_tabController.addListener(() {
setState(() {
currentPageIndex = _tabController.index;
});
});
}
@override
void didUpdateWidget(MainPage oldWidget) {
super.didUpdateWidget(oldWidget);
changePage(location: widget.location);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: TabBarView(
controller: _tabController,
children: List.generate(menu.length, (index) =>
KeepAliveClient(
child: menu[index]['page'] as Widget
)
)
),
bottomNavigationBar: NavigationBar(
destinations: [...]
selectedIndex: currentPageIndex,
onDestinationSelected: (int index) {
context.go(menu[index]['path']);
},
),
);
}
}
Even though it worked, I still felt like something was wrong. It would be great if someone could show me some more professional way
Upvotes: 0
Views: 4500
Reputation: 11
You can just use
StatefulShellRoute(
builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) {
return navigationShell;
},
navigatorContainerBuilder: (BuildContext context, StatefulNavigationShell navigationShell, List<Widget> children) {
return TabbedRootScreen(navigationShell: navigationShell, children: children);
},
branches: [...]
)
and then on TabbedRootScreen use these children in TabBarView
Upvotes: 1
Reputation: 4577
I would definitely go with the StatefulShellRoute. It is a little more complicated to setup but it is worth it and you can even scale it up to a responsive design afterward. I recently wrote an article on this that will also explain how to unit test it. You can find the article here.
A more simplified version than what I wrote in my article would look like this ( I use a StatefulShellRoute.indexedStack that simplify things a little bit and will make each branch correspond to an index starting at 0 and corresponding to each tab in order)
class AppRouter {
static final GoRouter simpleRouter = GoRouter(
routes: <RouteBase>[
GoRoute(
name: RoutesNames.home,
path: '/',
redirect: (_, __) => Routes.articles(),
),
StatefulShellRoute.indexedStack(
pageBuilder: (context, state, navigationShell) => FadeTransitionPage(
key: state.pageKey,
child: HomeView(nestedNavigator: NestedNavigator(navigationShell)),
),
branches: <StatefulShellBranch>[
StatefulShellBranch(
routes: <GoRoute>[
GoRoute(
name: RoutesNames.articles,
path: '/${PathSegments.articles}',
builder: (BuildContext context, GoRouterState state) =>
const ArticleListView(),
routes: <GoRoute>[
GoRoute(
name: RoutesNames.articleDetails,
path: ':${PathParams.articleId}',
builder: (BuildContext context, GoRouterState state) =>
ArticleDetailsView(articleId: articleId),
),
],
),
],
),
StatefulShellBranch(
routes: <GoRoute>[
GoRoute(
name: RoutesNames.user,
path: '/${PathSegments.user}',
builder: (BuildContext context, GoRouterState state) =>
const Center(child: Text('User page')),
),
],
),
],
),
],
debugLogDiagnostics: true,
);
}
The NestedNavigator I use is just a wrapper:
class NestedNavigator {
final StatefulNavigationShell statefulNavigationShell;
NestedNavigator(this.statefulNavigationShell);
Widget get navigatorContainer => statefulNavigationShell;
int get currentIndex => statefulNavigationShell.currentIndex;
void goBranch(int index) => statefulNavigationShell.goBranch(index);
}
And the HomeView will use statefulNavigationShell as state management for the current tab and the navigation trough the goBranch
method. goBranch is what allows to keep navigation state between tabs.
class HomeView extends StatefulWidget {
final NestedNavigator nestedNavigator;
const HomeView({super.key, required this.nestedNavigator});
@override
State<StatefulWidget> createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
/// this is the list of our tabs
final _destinations = const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.article_outlined),
activeIcon: Icon(Icons.article),
label: 'Articles',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_2_outlined),
activeIcon: Icon(Icons.person_2),
label: 'User',
),
];
/// This is a simple helper method to convert NavBar items to NavRail items
NavigationRailDestination _toRailDestination(BottomNavigationBarItem item) =>
NavigationRailDestination(
label: Text(item.label!),
icon: item.icon,
selectedIcon: item.activeIcon,
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: AdaptiveLayout(
internalAnimations: false,
primaryNavigation: SlotLayout(
config: <Breakpoint, SlotLayoutConfig>{
// On medium screen we will use a navigation rail
Breakpoints.medium: SlotLayout.from(
key: const Key('Primary Navigation Medium'),
builder: (_) => AdaptiveScaffold.standardNavigationRail(
padding: EdgeInsets.zero,
selectedIndex: widget.nestedNavigator.currentIndex,
onDestinationSelected: goToTab,
destinations: _destinations
.map((item) => _toRailDestination(item))
.toList(),
),
),
// On large screens we will use a drawer (extended version of the rail)
Breakpoints.large: SlotLayout.from(
key: const Key('Primary Navigation Large'),
builder: (_) => AdaptiveScaffold.standardNavigationRail(
padding: EdgeInsets.zero,
selectedIndex: widget.nestedNavigator.currentIndex,
onDestinationSelected: goToTab,
extended: true,
destinations: _destinations
.map((item) => _toRailDestination(item))
.toList(),
),
),
},
),
// BottomNavigation is only active in small views defined as under 600 dp
bottomNavigation: SlotLayout(
config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.small: SlotLayout.from(
key: const Key('Bottom Navigation Small'),
builder: (_) => BottomNavigationBar(
items: _destinations,
currentIndex: widget.nestedNavigator.currentIndex,
onTap: goToTab,
),
)
},
),
// Body will just contain the nested navigator for all screen sizes.
// Each nested view could eventually use adaptiveLayout again if needed
// Hence we won't use a secondary body in our home_view
body: SlotLayout(
config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.smallAndUp: SlotLayout.from(
key: const Key('Nested navigator'),
builder: (_) => widget.nestedNavigator.navigatorContainer,
),
},
),
),
);
}
void goToTab(int index) {
widget.nestedNavigator.goBranch(index);
}
}
Upvotes: 0