Search code examples
flutterdartwebroutesnavigation

Flutter push some NavigationRailDestination to bottom of the page


I'm trying to push some navigation rail destination to the bottom of the menu. How can I do it ? I want to still be able to navigate between pages but just have like an expanded widget after "facture" so that "Compte" and "déconexion" are at the bottom of the menu.

I tried using trailing property but it doesn't work since the navigationRailDestination are not widgets. I tried to put a SizedBox.expand() in the middle of the tabs list but doesn't work either. Any idea is welcome.

enter image description here

Here is my code :

class ScaffoldWithNavigationRail extends StatefulWidget {
  Widget child;

  ScaffoldWithNavigationRail({required this.child, Key? key}) : super(key: key);

  @override
  State<ScaffoldWithNavigationRail> createState() => _ScaffoldWithNavigationRailState();
}

class _ScaffoldWithNavigationRailState extends State<ScaffoldWithNavigationRail> {
  static const tabs = [
    ScaffoldWithNavigationRailDestination(
      icon: Icon(Icons.wine_bar),
      initialLocation: "/degustations",
      label: Text("Dégustation")
    ),
    ScaffoldWithNavigationRailDestination(
        icon: Icon(Icons.favorite),
        initialLocation: "/mes_degustations",
        label: Text("Mes dégustations")
    ),
    ScaffoldWithNavigationRailDestination(
        icon: Icon(Icons.payments),
        initialLocation: "/factures",
        label: Text("Factures")
    ),
    ScaffoldWithNavigationRailDestination(
        icon: Icon(Icons.person_2_rounded),
        initialLocation: "/mon_compte",
        label: Text("Mon Compte")
    ),
    ScaffoldWithNavigationRailDestination(
        icon: Icon(Icons.logout_outlined),
        initialLocation: "/log_out",
        label: Text("Déconexion")
    ),
  ];

  // getter that computes the current index from the current location,
  // using the helper method below
  int get _currentIndex => _locationToTabIndex(GoRouter.of(context).location);

  int _locationToTabIndex(String location) {
    final index =
    tabs.indexWhere((t) => location.startsWith(t.initialLocation));
    // if index not found (-1), return 0
    return index < 0 ? 0 : index;
  }

  // callback used to navigate to the desired tab
  void _onItemTapped(BuildContext context, int tabIndex) {
    if (tabIndex != _currentIndex) {
      // go to the initial location of the selected tab (by index)
      context.go(tabs[tabIndex].initialLocation);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          SafeArea(
            child: NavigationRail(
              elevation: 10,
              extended: true,
              destinations: tabs,
              selectedIndex: _currentIndex,
              onDestinationSelected: (index) => _onItemTapped(context, index),
              
            ),
          ),
          Expanded(child: widget.child)
        ],
      ),
    );
  }
}

edit :

Here is the ScaffoldWithNavigationRailDestination code :

class ScaffoldWithNavigationRailDestination extends NavigationRailDestination {
  const ScaffoldWithNavigationRailDestination({required this.initialLocation, required super.icon, required super.label});
  final String initialLocation;
}

I use go_router package to navigate through the pages. Here is my app.dart file :

// private navigators
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();

// GoRouter configuration
final _router = GoRouter(
  initialLocation: RouteNames.tasting,
  navigatorKey: _rootNavigatorKey,
  redirect: (BuildContext context, GoRouterState state) {
    if (state.path == "/") return RouteNames.login;
    return null;
  },
  routes: [
    ShellRoute(
      navigatorKey: _shellNavigatorKey,
      builder: (context, state, child) {
        return ScaffoldWithNavigationRail(child: child);
      },
      routes: [
        GoRoute(
            path: RouteNames.tasting,
            pageBuilder: (context, state) => const NoTransitionPage(
              child: Degustation(),
            ),
        ),
        GoRoute(
          path: RouteNames.myTasting,
          pageBuilder: (context, state) => const NoTransitionPage(
            child: Degustation(),
          ),
        ),
        GoRoute(
          path: RouteNames.bills,
          pageBuilder: (context, state) => const NoTransitionPage(
            child: Degustation(),
          ),
        ),
        GoRoute(
          path: RouteNames.account,
          pageBuilder: (context, state) => const NoTransitionPage(
            child: Degustation(),
          ),
        ),
        GoRoute(
          path: RouteNames.logout,
          pageBuilder: (context, state) => const NoTransitionPage(
            child: Degustation(),
          ),
        ),
      ]
    ),
    GoRoute(
      path: RouteNames.register,
      pageBuilder: (context, state) => NoTransitionPage(
        child: RegisterPage(),
      ),
    ),
    GoRoute(
      path: RouteNames.login,
      pageBuilder: (context, state) => NoTransitionPage(
        child: LoginPage(),
      ),
    ),
  ]
);

/// The Widget that configures your application.
class MyApp extends StatelessWidget {
  final AuthRepository authRepository;
  const MyApp({
    super.key,
    required this.authRepository,
  });

  @override
  Widget build(BuildContext context) {
    return MultiRepositoryProvider(
      providers: [
        RepositoryProvider.value(value: authRepository)
      ],
    child: MaterialApp.router(
      routerConfig: _router,
      restorationScopeId: 'wine',

      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: const [
        Locale('fr', ''),
      ],

      onGenerateTitle: (BuildContext context) =>
          AppLocalizations.of(context)!.appTitle,

      theme: ThemeData(
        useMaterial3: true
      ),
    ),
);
  }
}

the RouteNames is just a file containing static const String with the route path.

Thanks a lot for your help !


Solution

  • Based on the issue [NavigationRail] Ability to add vertical space between icons #69193.

    Yes, basically adding a padding parameter that takes an EdgeInsetsGeometry would be consistent with the rest of the framework and be the most flexible approach. We also want to make sure that we correctly decide whether or not to wrap the padding with InkResponse. Padding for buttons tend to place the Padding widget so that its tappable space grows within the bounds of the button, so I assume this is probably what's desired here as well?

    The flutter team have just added the padding , which wont help you in adding a Spacer property between the destinations so there is currently no way that you could do this using NavigationRail provided by flutter . So you have take aid of CustomNavigationRail


    Here is a sample CustomNavigationRail designed based on your needs:

              CustomNavigationRail(
                 spacerIndex: 2,       // Emphasis on the this
                 destinations: [
                   DestinationItem(
                     icon: Icons.home,
                     label: 'Home',
                   ),
                   DestinationItem(
                     icon: Icons.settings,
                     label: 'Settings',
                   ),
                   DestinationItem(
                     icon: Icons.logout,
                     label: 'Logout',
                   ),
                 ],
                 selectedIndex: _selectedIndex,
                 onDestinationSelected: (int index) {
                   setState(() {
                     _selectedIndex = index;
                   });
                 },
               ),
    
    

    Here there is a prop named spacerIndex which is used to place the Spacer between navigations, if you have 4 destination widgets, and you add spacerIndex: 2, It would add Spacer after the second widget.


    Complete code:

    import 'package:flutter/material.dart';
    
    class CustomNavigation<T> extends StatefulWidget {
      CustomNavigation({Key? key, this.save, this.delete}) : super(key: key);
    
      final void Function(T)? save;
      final void Function(T)? delete;
    
      @override
      State<CustomNavigation<T>> createState() => _CustomNavigationState<T>();
    }
    
    class _CustomNavigationState<T> extends State<CustomNavigation<T>> {
      final List<Widget> _pages = [
        HomePage(),
        SettingsPage(),
        LogoutPage(),
      ];
    
      int _selectedIndex = 0;
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
            title: 'NavigationRail Demo',
            home: Scaffold(
              appBar: AppBar(title: Text('NavigationRail Demo')),
              body: Row(
                children: [
                  CustomNavigationRail(
                    spacerIndex: 2,
                    destinations: [
                      DestinationItem(
                        icon: Icons.home,
                        label: 'Home',
                      ),
                      DestinationItem(
                        icon: Icons.settings,
                        label: 'Settings',
                      ),
                      DestinationItem(
                        icon: Icons.logout,
                        label: 'Logout',
                      ),
                    ],
                    selectedIndex: _selectedIndex,
                    onDestinationSelected: (int index) {
                      setState(() {
                        _selectedIndex = index;
                      });
                    },
                  ),
                  Expanded(
                    child: _pages[_selectedIndex],
                  ),
                ],
              ),
            ));
      }
    }
    
    class DestinationItem {
      final IconData icon;
      final String label;
    
      const DestinationItem({
        Key? key,
        required this.icon,
        required this.label,
      });
    }
    
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Text('Home Page'),
        );
      }
    }
    
    class SettingsPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Text('Settings Page'),
        );
      }
    }
    
    class LogoutPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Text('Logout Page'),
        );
      }
    }
    
    class CustomNavigationRail extends StatefulWidget {
      final double? elevation;
      final List<DestinationItem> destinations;
      final int selectedIndex;
      final int? spacerIndex;
      final ValueChanged<int> onDestinationSelected;
    
      const CustomNavigationRail({
        Key? key,
        this.elevation,
        this.spacerIndex,
        required this.destinations,
        required this.selectedIndex,
        required this.onDestinationSelected,
      }) : super(key: key);
    
      @override
      _CustomNavigationRailState createState() => _CustomNavigationRailState();
    }
    
    class _CustomNavigationRailState extends State<CustomNavigationRail> {
      @override
      Widget build(BuildContext context) {
        return (widget.spacerIndex ?? 0) > widget.destinations.length
            ? const Center(
                child: Text(
                    "Exception: SpacerIndex Can't be less than length of destination"))
            : Card(
                elevation: widget.elevation ?? 0,
                child: Row(
                  children: [
                    Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          mainAxisSize: MainAxisSize.min,
                          children: widget.destinations
                              .sublist(0, widget.spacerIndex ?? 0)
                              .map((destination) {
                            final index = widget.destinations.indexOf(destination);
                            return GestureDetector(
                              onTap: () => widget.onDestinationSelected(index),
                              child: _CustomNavigationRailDestination(
                                child: destination,
                                selected: index == widget.selectedIndex,
                              ),
                            );
                          }).toList(),
                        ),
                        if (widget.spacerIndex != null) Spacer(),
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            ...widget.destinations
                                .sublist(widget.spacerIndex ?? 0)
                                .map((destination) {
                              final index =
                                  widget.destinations.indexOf(destination);
                              return GestureDetector(
                                onTap: () => widget.onDestinationSelected(index),
                                child: _CustomNavigationRailDestination(
                                  child: destination,
                                  selected: index == widget.selectedIndex,
                                ),
                              );
                            }).toList(),
                          ],
                        ),
                      ],
                    ),
                  ],
                ),
              );
      }
    
    }
    
    class _CustomNavigationRailDestination extends StatelessWidget {
      final DestinationItem child;
      final bool selected;
    
      const _CustomNavigationRailDestination({
        Key? key,
        required this.child,
        required this.selected,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        final color =
            selected ? Theme.of(context).primaryColor : Colors.grey.shade600;
        final textTheme = Theme.of(context).textTheme.bodyLarge;
        final textStyle =
            textTheme?.copyWith(color: color) ?? TextStyle(color: color);
    
        return SizedBox(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(child.icon, color: color),
                SizedBox(width: 16),
                Text(child.label, style: textStyle),
              ],
            ),
          ),
        );
      }
    }
    
    

    Output:

    1. with spacerIndex: 2

    enter image description here

    1. with spacerIndex: 1

    enter image description here

    Feel free to change any characters based on your requirements, I have created a minimal sample code, which can lead you to more generic and advanced customized widget based on your needs.