UPDATE (RECOMMENDED):
I have now developed a package to tackle this very problem.
Using this go_router_tabs package the solution is much simpler with only a few extra lines of code:
import 'package:flutter/material.dart';
import 'package:go_router_tabs/go_router_tabs.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: GoRouter(
initialLocation: "/dash",
routes: [
TabShellRoute(
builder: (context, state, index, child) => SideNavBarPage(
selectedIndex: index,
subPage: child,
),
childPageBuilder: (context, state, direction, child) {
return TabTransitionPage(
key: state.pageKey,
direction: direction,
transitionsBuilder: TabTransitionPage.verticalPushTransition,
child: child,
);
},
routes: [
GoRoute(
path: "/dash",
builder: (context, state) => DashPage(),
),
GoRoute(
path: "/other",
builder: (context, state) => OtherPage(),
),
GoRoute(
path: "/another",
builder: (context, state) => AnotherPage(),
),
],
).toShellRoute,
],
),
);
}
}
class SideNavBarPage extends StatelessWidget {
final int selectedIndex;
final Widget subPage;
const SideNavBarPage({
required this.selectedIndex,
required this.subPage,
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
CustomSideNavBar(
selectedIndex: selectedIndex,
items: [
CustomSideNavBarItem(onTap: () => context.go("/dash")),
CustomSideNavBarItem(onTap: () => context.go("/other")),
CustomSideNavBarItem(onTap: () => context.go("/another")),
],
),
Expanded(child: subPage),
],
),
);
}
}
This solution removes the need for global controllers. The package can also handle nested navigation bar setups and will always know which navigation item is selected no matter how deeply nested the current route is and how the user got there.
ORIGINAL ANSWER (NO LONGER RECOMMENDED):
First let's create a controller that updates the selected tab in the navigation rail. It will therefore be able calculate the direction of the slide transition:
class NavRailController {
/// The paths of the routes represented by the navigation rail.
final routePaths = <String>["/1", "/2", "/3"];
/// The index of the tab currently displayed.
var currentTabIndex = 0;
/// The index of the tab last displayed.
var _previousTabIndex = 0;
/// The direction of a slide transition.
TextDirection slideDirection() {
return currentTabIndex >= _previousTabIndex
? TextDirection.rtl
: TextDirection.ltr;
}
/// Used in the [GoRouter] redirect to update the selected tab in the
/// navigation rail.
String? redirect(BuildContext context, GoRouterState state) {
_previousTabIndex = currentTabIndex;
currentTabIndex = routePaths.indexWhere(
(path) => state.location.contains(path),
);
return null;
}
}
By adding the redirect
method of the controller to the GoRouter
we can cause the navigation bar to update every time we navigate to a new page:
final navRailController = NavRailController();
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: GoRouter(
initialLocation: navRailController.routePaths[0],
redirect: navRailController.redirect,
routes: [
// ...
],
),
);
}
}
This is an example page with the navigation rail:
class NavRailPage extends StatelessWidget {
final Widget child;
const NavRailPage({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: navRailController.currentTabIndex,
onDestinationSelected: (value) => context.go(
navRailController.routePaths[value],
),
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
label: Text("1"),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
label: Text("2"),
),
NavigationRailDestination(
icon: Icon(Icons.star_border),
label: Text("3"),
),
],
),
Expanded(child: child),
],
),
);
}
}
This is an example page displayed next to the navigation rail.
class NavRailItemPage extends StatelessWidget {
final String title;
const NavRailItemPage(this.title, {super.key});
@override
Widget build(BuildContext context) {
return Center(child: Text(title));
}
}
Now let's create a GoRouter
CustomTransitionPage
that let's us control the direction of the slide transition:
class SlideTransitionPage extends CustomTransitionPage {
SlideTransitionPage({
super.key,
required TextDirection Function() direction,
required super.child,
}) : super(
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final dir = direction();
final slideTween = Tween<Offset>(
begin: dir == TextDirection.ltr
? const Offset(0, -1)
: const Offset(0, 1),
end: const Offset(0, 0),
);
final secondarySlideTween = Tween<Offset>(
begin: const Offset(0, 0),
end: dir == TextDirection.ltr
? const Offset(0, 1)
: const Offset(0, -1),
);
return SlideTransition(
position: slideTween.animate(animation),
child: SlideTransition(
position: secondarySlideTween.animate(secondaryAnimation),
child: child,
),
);
},
);
}
Now we can add our routes to the GoRouter
in MyApp
:
routes: [
ShellRoute(
builder: (context, state, child) => NavRailPage(child: child),
routes: [
GoRoute(
path: navRailController.routePaths[0],
pageBuilder: (context, state) => SlideTransitionPage(
key: state.pageKey,
direction: navRailController.slideDirection,
child: const NavRailItemPage("1"),
),
),
GoRoute(
path: navRailController.routePaths[1],
pageBuilder: (context, state) => SlideTransitionPage(
key: state.pageKey,
direction: navRailController.slideDirection,
child: const NavRailItemPage("2"),
),
),
GoRoute(
path: navRailController.routePaths[2],
pageBuilder: (context, state) => SlideTransitionPage(
key: state.pageKey,
direction: navRailController.slideDirection,
child: const NavRailItemPage("3"),
),
)
],
),
],