skip to Main Content

I have a Flutter web app with a custom side navigation bar which is displayed on every page. So that I can always show the same side navigation bar while still keeping URL navigation I am using the go_router package with all of the pages in a ShellRoute.
Now I want to slide transition up to a navigation item below the current one and visa-verse. How can I achieve this?

Here’s an over simplified version of my code

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: GoRouter(
        initialLocation: "/dash",
        routes: [
          ShellRoute(
            builder: (context, state, child) => SideNavBarPage(subPage: child),
            routes: [
              GoRoute(
                path: '/dash',
                builder: (context, state) => DashPage(),
              ),
              GoRoute(
                path: '/other',
                builder: (context, state) => OtherPage(),
              ),
              GoRoute(
                path: '/another',
                builder: (context, state) => AnotherPage(),
              ),
            ],
          ),
        ],
      ),
    );
  }
}


class SideNavBarPage extends StatelessWidget {
  final Widget subPage;

  const SideNavBarPage({
    required this.subPage,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          CustomSideNavBar(
            items: [
              CustomSideNavBarItem(onTap: () => context.go("/dash")),
              CustomSideNavBarItem(onTap: () => context.go("/other")),
              CustomSideNavBarItem(onTap: () => context.go("/another")),
            ],
          ),
          Expanded(child: subPage),
        ],
      ),
    );
  }
}

For example: if I wanted to switch from /another to /dash I would want to transition down from top to bottom, whereas from /dash to /other I would want to slide up from bottom to top.

2

Answers


  1. Chosen as BEST ANSWER

    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"),
                    ),
                  )
                ],
              ),
            ],
    

  2. Use auto_route: ^7.1.0 for navigation in flutter, it have transitionBuilder so that you can simply enable transition animation between pages.

    Eg:

    class DashboardPage extends StatelessWidget {                
    @override                
    Widget build(BuildContext context) {                
    return AutoTabsRouter(                
    // list of your tab routes                
    // routes used here must be declared as children                
    // routes of /dashboard                 
      routes: const [                
        UsersRoute(),                
        PostsRoute(),                
        SettingsRoute(),                
      ],          
      transitionBuilder: (context,child,animation)=> FadeTransition(                
              opacity: animation,                
              // the passed child is technically our animated selected-tab page                
              child: child,                
            ),       
      builder: (context, child) {                
        // obtain the scoped TabsRouter controller using context                
        final tabsRouter = AutoTabsRouter.of(context);                
        // Here we're building our Scaffold inside of AutoTabsRouter                
        // to access the tabsRouter controller provided in this context                
        //                 
        //alterntivly you could use a global key                
        return Scaffold(                
            body: child,               
            bottomNavigationBar: BottomNavigationBar(                
              currentIndex: tabsRouter.activeIndex,                
              onTap: (index) {                
                // here we switch between tabs                
                tabsRouter.setActiveIndex(index);                
              },                
              items: [                
                BottomNavigationBarItem(label: 'Users',...),                
                BottomNavigationBarItem(label: 'Posts',...),                
                BottomNavigationBarItem(label: 'Settings',...),                
              ],                
            ));                
      },                
    );}}
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search