Persisting AppBar Drawer across all Pages Flutter
Asked Answered
A

7

59

I am trying to create a uniform drawer that is accessible across all pages in my app. How do I make it persist throughout all these pages without having to recreate my custom Drawer widget in every single dart file?

Actinal answered 2/8, 2018 at 18:7 Comment(1)
Check out this repo Navigation Drawer with Multiple Fragments using blocScamper
T
86

There are a few different options for this. The most basic is hopefully something you've already done, but I'll list it anyways:

1: Create a class for your drawer

Your widget should be its own stateful or stateless widget. This way, you just have to instantiate it each time.

class MyDrawer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Drawer(...);
  }
}

And then when using it in each page:

Scaffold(
  drawer: MyDrawer(...),
  ...
)

I hope you're already doing this; if not you should be. A class's build function shouldn't be too large or it can lead to poor performance and harder to maintain code; splitting things into logical units will help you in the long run.

2: Create a class for your scaffold

If having to include the same drawer in a scaffold for each page is still too much code, you can instead use a class that encapsulates your scaffold. It would essentially take inputs for each of the scaffold inputs you actually use.

class MyScaffold extends StatelessWidget {

  final Widget body;

  MyScaffold({this.body});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
     body: body,
     drawer: MyDrawer(...),
    );
  }
}

And then instead of using Scaffold in your code, use MyScaffold (but please name it something better =D).

3: Multi level scaffold

I'm only including this way of doing it to be complete, and I don't recommend it. That being said, there are certain things you can't get to work within flutter's normal workflow that you could do by doing this - for example if you want a custom animation for when the user taps on different items in the drawer.

Basically, what you'd do in this case is to have a Scaffold outside of your MaterialApp or Navigator (which I believe would also mean you'd have to have another Navigator outside that, but I'm not 100% sure). You would have the scaffold that's outside your navigation show the drawer while the other one (on each page within the navigation) would do whatever else you need it to do. There's a few caveats - you'd have to make sure you get the right scaffold (i.e. Scaffold.of(context) by itself wouldn't cut it - you'd have to get the context of the first scaffold and use it to find the higher-level one), and you'd probably need to pass a GlobalKey (of the lower-level scaffold) to the Drawer so that it could actually change pages within it.

As I said, I don't recommend this approach, so I'm not going to go into any more detail than that but rather leave it as an exercise for the reader if they want to go down that rabbit hole!

Takeshi answered 2/8, 2018 at 20:1 Comment(5)
Honestly, I'd only recommend the 1st solution. That's definitely how you should work in flutterExsert
Fair, and in this case I'd agree. But say if you had an app bar, a bottom bar, a FAB, and two drawers that were all identical across multiple pages of your app, you might want to start considering an approach that didn't necessitate copy/pasting that each time you add a new page - in which case the second option is a workable solution.Takeshi
Yeah depends. Anyway, for the 3rd point, you can work with 2 scaffold at once easily now. Thanks to context.rootAncestorStateOfType. This removes the need of using a GlobalKey in most situationsExsert
added a more flutterish 3rd solution. Feel free to take a look :)Exsert
how to hide status bar in drawer but not in rest of the applicationSeverini
J
48

rmtmckenzie is very correct.

Although if you are curious about the multi scaffold solution, this can be more elegant than you think.

To share a drawer between all pages we could add a builder in our MaterialApp instance. This will instantiate a Scaffold under Navigator but above all routes.

MaterialApp(
  title: 'Flutter Demo',
  builder: (context, child) {
    return Scaffold(
      drawer: MyDrawer(),
      body: child,
    );
  },
  home: MyHome()
)

Inside your page, you can instantiate another Scaffold without restriction as you'd usually do.

You can then show the shared drawer by doing the following in any widget under MaterialApp :

final ScaffoldState scaffoldState = context.rootAncestorStateOfType(TypeMatcher<ScaffoldState>());
scaffoldState.openDrawer();

Code which you can extract into a nice helper :

class RootScaffold {
  static openDrawer(BuildContext context) {
    final ScaffoldState scaffoldState =
        context.rootAncestorStateOfType(TypeMatcher<ScaffoldState>());
    scaffoldState.openDrawer();
  }
}

Then reuse using RootScaffold.openDrawer(context)

Jermayne answered 2/8, 2018 at 20:47 Comment(7)
That's the true answer . i believeOxonian
If placed in your main, this might have some limitations like access to Drawer in the routes where you don't want the drawer. ex. Login/Signup pageCompetence
So you'd have to manually add the hamburger icon to your AppBar and call the function to open the drawer? Which I guess is ok, cause then you can choose when it shows.Showplace
What is the way then to close the drawer? I tried using navigator in list tiles in the drawer but it says "Navigator operation requested with a context that does not include a Navigator"Antilog
Ok, I see in the more popular answer now that it's actually better for what I need. Then the drawer get's a context that has the current Navigation functional, like Navigator.of(context).pop()Antilog
The "scaffoldState" portion of the code above has changed slightly (2022)...see my response below for the updated code.Logan
unable to navigate to other screen by using this .Excellency
O
12

In Addition to @Rémi Rousselet Answer

MaterialApp(
 title: 'Flutter Demo',
 builder: (context, child) {
   return Scaffold(
     drawer: MyDrawer(),
     body: child,
   );
 },
 home: MyHome()
)

For Navigation in root drawer if you use Navigator.of(context) // push or pop that will throw error and for that you must use child widget to navigate to different pages

Like that

(child.key as GlobalKey<NavigatorState>).currentState // push or pop

Demo project in Github

Oxonian answered 25/12, 2018 at 8:5 Comment(8)
Works perfect, but is it possible to exclude some page from building a drawer or other staff like bottomNavbar? Just to create it everywhere except this page.Deputation
You welcome, in my opinion it's not possible because NavigationDrawer technically drawn over app (all page , routing and other staff) for thing like that you could use @Takeshi first option.Oxonian
this is a great sol. but wonder how do i close drawer on item clicked?Mireille
nvm i got it in the demo using RootDrawer classMireille
@Mireille try to pop drawer with this (child.key as GlobalKey<NavigatorState>).currentStateOxonian
It didn't work, had it use closedrawer function, its in the demo you mentionedMireille
@Mireille Oh that's my bad , you can close it with RootDrawer.of(context).close(); like i done in demoOxonian
Yea, and pop() doesn't close drawer, it pops the state below drawerMireille
L
3

In Addition to @Rémi Rousselet Answer, the code has slightly changed (2022) - due to null safety amends. Replace this:

class RootScaffold {
   static openDrawer(BuildContext context) {
      final ScaffoldState scaffoldState =
       context.rootAncestorStateOfType(TypeMatcher<ScaffoldState>());
      scaffoldState.openDrawer();
   }
}

...with...

  class RootScaffold {
    static openDrawer(BuildContext context) {    
       final ScaffoldState? scaffoldState = context.findRootAncestorStateOfType<ScaffoldState>();
       scaffoldState?.openDrawer();
    }
  }
Logan answered 10/3, 2022 at 9:41 Comment(1)
You beat me to it.Afterclap
C
2

if somebody looking for fancy stuff while navigating look here. What I use as a drawer for my project is flutter_inner_drawer package.

I created a stateful class named CustomDrawer.

class CustomDrawer extends StatefulWidget {
  final Widget scaffold;
  final GlobalKey<InnerDrawerState> innerDrawerKey;
  CustomDrawer({
    Key key,
    this.scaffold,
    this.innerDrawerKey,
  }) : super(key: key);
  @override
  _CustomDrawerState createState() => _CustomDrawerState();
}

class _CustomDrawerState extends State<CustomDrawer> {

  MainPageIcons assets = MainPageIcons();//From my actual code dont care it
  final vars = GlobalVars.shared; //From my actual code dont care it

  @override
  Widget build(BuildContext context) {
    return InnerDrawer(
      key: widget.innerDrawerKey,
        onTapClose: true, // default false
        tapScaffoldEnabled: true,
        swipe: true, // default true
        colorTransition: Colors.teal, // default Color.black54
        //innerDrawerCallback: (a) => print(a ),// return bool
        leftOffset: 0.2, // default 0.4
        leftScale: 1,// default 1
        boxShadow: [
        BoxShadow(color: Colors.teal,blurRadius: 20.0, // has the effect of softening the shadow
        spreadRadius: 10.0, // has the effect of extending the shadow
        offset: Offset(
        10.0, // horizontal, move right 10
        10.0, // vertical, move down 10
    ),)],
    borderRadius: 20, // default 0
    leftAnimationType: InnerDrawerAnimation.quadratic, // default static
    //when a pointer that is in contact with the screen and moves to the right or left
      onDragUpdate: (double val, InnerDrawerDirection direction) =>
          setState(() => _dragUpdate = val),
      //innerDrawerCallback: (a) => print(a),

    // innerDrawerCallback: (a) => print(a), // return  true (open) or false (close)
    leftChild: menus(), // required if rightChild is not set

    scaffold:widget.scaffold
    );
  }
  double _dragUpdate = 0;
  Widget menus(){
    return
      Material(
          child: Stack(
            children: <Widget>[
              Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topRight,
                    end: Alignment.bottomLeft,
                    colors: [
                      ColorTween(
                        begin: Colors.blueAccent,
                        end: Colors.blueGrey[400].withRed(100),
                      ).lerp(_dragUpdate),
                      ColorTween(
                        begin: Colors.green,
                        end: Colors.blueGrey[800].withGreen(80),
                      ).lerp(_dragUpdate),
                    ],
                  ),
                ),
                child: Stack(
                  children: <Widget>[
                    Padding(
                      padding: EdgeInsets.only(left: 30),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                          Column(
                            children: <Widget>[
                              Container(
                                margin: EdgeInsets.only(left: 10, bottom: 15),
                                width: 80,
                                child: ClipRRect(
                                  child: Image.network(
                                    "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSrWfWLnxIT5TnuE-JViLzLuro9IID2d7QEc2sRPTRoGWpgJV75",
                                  ),
                                  borderRadius: BorderRadius.circular(60),
                                ),
                              ),
                              Text(
                                "User",
                                style: TextStyle(color: Colors.white, fontSize: 18),
                              )
                            ],
                            //mainAxisAlignment: MainAxisAlignment.center,
                          ),
                          Padding(
                            padding: EdgeInsets.all(10),
                          ),
                          ListTile(
                            onTap: ()=>navigate(Profile.tag),
                            title: Text(
                              "Profile",
                              style: TextStyle(color: Colors.white, fontSize: 14),
                            ),
                            leading: Icon(
                              Icons.dashboard,
                              color: Colors.white,
                              size: 22,
                            ),
                          ),

                          ListTile(
                            title: Text(
                              "Camera",
                              style: TextStyle(fontSize: 14,color:Colors.white),
                            ),
                            leading: Icon(
                              Icons.camera,
                              size: 22,
                              color: Colors.white,
                            ),
                            onTap: ()=>navigate(Camera.tag)
                          ),

                          ListTile(
                            title: Text(
                              "Pharmacies",
                              style: TextStyle(fontSize: 14,color:Colors.white),
                            ),
                            leading: Icon(
                              Icons.add_to_photos,
                              size: 22,
                              color: Colors.white,
                            ),
                            onTap: ()=>navigate(Pharmacies.tag)
                          ),
                        ],
                      ),
                    ),
                    Positioned(
                      bottom: 20,
                      child: Container(
                        alignment: Alignment.bottomLeft,
                        margin: EdgeInsets.only(top: 50),
                        padding: EdgeInsets.symmetric(vertical: 15, horizontal: 25),
                        width: double.maxFinite,
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.start,
                          children: <Widget>[
                            Icon(
                              Icons.all_out,
                              size: 18,
                              color: Colors.grey,
                            ),
                            Text(
                              " LogOut",
                              style: TextStyle(
                                fontSize: 16,
                                color: Colors.grey,
                              ),
                            ),
                          ],
                        ),
                      ),
                    )
                  ],
                ),
              ),
              _dragUpdate < 1
                  ? BackdropFilter(
                filter: ImageFilter.blur(
                    sigmaX: (10 - _dragUpdate * 10),
                    sigmaY: (10 - _dragUpdate * 10)),
                child: Container(
                  decoration: BoxDecoration(
                    color: Colors.black.withOpacity(0),
                  ),
                ),
              )
                  : null,
            ].where((a) => a != null).toList(),
          ));
  }

  navigate(String route) async{
    await navigatorKey.currentState.pushNamed(route).then((_){
      Timer(Duration(milliseconds: 500),()=>widget.innerDrawerKey.currentState.toggle() );

    });
  }

}

I copied example from package and didnt touch original much. only aded a function to toggle after turn back.

navigate(String route) async{
        await navigatorKey.currentState.pushNamed(route).then((_){
          Timer(Duration(milliseconds: 500),()=>widget.innerDrawerKey.currentState.toggle() );

        });
      }

to navigate from all over pages aded GlobalKey globally so that reachable from every class

final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: "Main Navigator");

inner_drawer also needs a globalkey for state to toogle but if you create only one when navigate between pages it gives duplicate global key error. to avoid I created a global variable named innerKeys

 Map<String,GlobalKey<InnerDrawerState>>innerKeys={
    'main':GlobalKey<InnerDrawerState>(),
    'profile':GlobalKey<InnerDrawerState>(),
    'pharmacies':GlobalKey<InnerDrawerState>(),

  };

finally I added this CustomDrawer to every pages

 @override
  Widget build(BuildContext context) {
    return CustomDrawer(
        innerDrawerKey: vars.innerKeys['profile'],
        scaffold:Scaffold(
        appBar: CustomAppBar(
          title: 'Profile',
          actions: <Widget>[
          ],),
        body: Stack(
                children: <Widget>[
                  Background(),
                ])));
  }

I hope it will helps to someone.

NOTE: please check original flutter pack if anything updated. Be avare that this example is not perfect and needs to taken care that if many navigation over this drawer then widget tree will have many pages and performance will be impacted. any tuning suggestion will be appriciated.

Chitchat answered 20/10, 2019 at 22:49 Comment(0)
S
2

My Solution Navigation Drawer with Multiple Fragments using bloc package

First, add below dependencies in your pubspec.yaml file

flutter_bloc: ^4.0.0

Now create below files

drawer_event.dart

import 'nav_drawer_state.dart';
abstract class NavDrawerEvent {
const NavDrawerEvent();
}
class NavigateTo extends NavDrawerEvent {
final NavItem destination;
const NavigateTo(this.destination);
}

nav_drawer_bloc.dart

import 'package:bloc/bloc.dart';

import 'drawer_event.dart';
import 'nav_drawer_state.dart';

class NavDrawerBloc extends Bloc<NavDrawerEvent, NavDrawerState> {

  @override
  NavDrawerState get initialState => NavDrawerState(NavItem.homePage);

  @override
  Stream<NavDrawerState> mapEventToState(NavDrawerEvent event) async* {
    if (event is NavigateTo) {
      if (event.destination != state.selectedItem) {
        yield NavDrawerState(event.destination);
      }
    }
  }
}

nav_drawer_state.dart

class NavDrawerState {
  final NavItem selectedItem;

  const NavDrawerState(this.selectedItem);
}

enum NavItem {
  homePage,
  profilePage,
  orderPage,
  myCart,
}

drawer_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutterdrawerwithbloc/bloc/drawer_event.dart';
import 'package:flutterdrawerwithbloc/bloc/nav_drawer_bloc.dart';
import 'package:flutterdrawerwithbloc/bloc/nav_drawer_state.dart';

class NavDrawerWidget extends StatelessWidget {
  final String accountName;
  final String accountEmail;
  final List<_NavigationItem> _listItems = [
    _NavigationItem(true, null, null, null),
    _NavigationItem(false, NavItem.homePage, "Home", Icons.home),
    _NavigationItem(false, NavItem.profilePage, "Profile Page", Icons.person),
    _NavigationItem(false, NavItem.orderPage, "My Orders", Icons.list),
    _NavigationItem(false, NavItem.myCart, "My Cart", Icons.shopping_cart),
  ];

  NavDrawerWidget(this.accountName, this.accountEmail);

  @override
  Widget build(BuildContext context) => Drawer(
          child: Container(
        child: ListView.builder(
            padding: EdgeInsets.zero,
            itemCount: _listItems.length,
            itemBuilder: (BuildContext context, int index) =>
                BlocBuilder<NavDrawerBloc, NavDrawerState>(
                  builder: (BuildContext context, NavDrawerState state) =>
                      _buildItem(_listItems[index], state),
                )),
      ));

  Widget _buildItem(_NavigationItem data, NavDrawerState state) =>
      data.header ? _makeHeaderItem() : _makeListItem(data, state);

  Widget _makeHeaderItem() => UserAccountsDrawerHeader(
        accountName: Text(accountName, style: TextStyle(color: Colors.white)),
        accountEmail: Text(accountEmail, style: TextStyle(color: Colors.white)),
        decoration: BoxDecoration(color: Colors.indigo),
        currentAccountPicture: CircleAvatar(
          backgroundColor: Colors.white,
          foregroundColor: Colors.amber,
          child: Icon(
            Icons.person,
            size: 54,
          ),
        ),
      );

  Widget _makeListItem(_NavigationItem data, NavDrawerState state) => Card(
        shape: ContinuousRectangleBorder(borderRadius: BorderRadius.zero),
        borderOnForeground: true,
        elevation: 0,
        margin: EdgeInsets.zero,
        child: Builder(
          builder: (BuildContext context) => ListTile(
            title: Text(
              data.title,
              style: TextStyle(
                color: data.item == state.selectedItem ? Colors.green : Colors.blueGrey,
              ),
            ),
            leading: Icon(
              data.icon,
              color: data.item == state.selectedItem ? Colors.green : Colors.blueGrey,
            ),
            onTap: () => _handleItemClick(context, data.item),
          ),
        ),
      );

  void _handleItemClick(BuildContext context, NavItem item) {
    BlocProvider.of<NavDrawerBloc>(context).add(NavigateTo(item));
    Navigator.pop(context);
  }
}

class _NavigationItem {
  final bool header;
  final NavItem item;
  final String title;
  final IconData icon;

  _NavigationItem(this.header, this.item, this.title, this.icon);
}

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutterdrawerwithbloc/bloc/nav_drawer_bloc.dart';
import 'package:flutterdrawerwithbloc/bloc/nav_drawer_state.dart';
import 'package:flutterdrawerwithbloc/drawer_widget.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Navigation Drawer Demo',
      theme: ThemeData(primarySwatch: Colors.blue, scaffoldBackgroundColor: Colors.white),
      home: MyHomePage(),
    );
    ;
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  NavDrawerBloc _bloc;
  Widget _content;

  @override
  void initState() {
    super.initState();
    _bloc = NavDrawerBloc();
    _content = _getContentForState(_bloc.state.selectedItem);
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => BlocProvider<NavDrawerBloc>(
      create: (BuildContext context) => _bloc,
      child: BlocListener<NavDrawerBloc, NavDrawerState>(
        listener: (BuildContext context, NavDrawerState state) {
          setState(() {
            _content = _getContentForState(state.selectedItem);
          });
        },
        child: BlocBuilder<NavDrawerBloc, NavDrawerState>(
          builder: (BuildContext context, NavDrawerState state) => Scaffold(
            drawer: NavDrawerWidget("AskNilesh", "[email protected]"),
            appBar: AppBar(
              title: Text(_getAppbarTitle(state.selectedItem)),
              centerTitle: false,
              brightness: Brightness.light,
              backgroundColor: Colors.indigo,
            ),
            body: AnimatedSwitcher(
              switchInCurve: Curves.easeInExpo,
              switchOutCurve: Curves.easeOutExpo,
              duration: Duration(milliseconds: 300),
              child: _content,
            ),
          ),
        ),
      ));

  _getAppbarTitle(NavItem state) {
    switch (state) {
      case NavItem.homePage:
        return 'Home';
      case NavItem.profilePage:
        return 'Profile Page';
      case NavItem.orderPage:
        return 'My Orders';
      case NavItem.myCart:
        return 'My Cart';
      default:
        return '';
    }
  }

  _getContentForState(NavItem state) {
    switch (state) {
      case NavItem.homePage:
        return Center(
          child: Text(
            'Home Page',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        );
      case NavItem.profilePage:
        return Center(
          child: Text(
            'Profile Page',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        );
      case NavItem.orderPage:
        return Center(
          child: Text(
            'My Orders',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        );
      case NavItem.myCart:
        return Center(
          child: Text(
            'My Cart',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        );
      default:
        return Center(
          child: Text(
            'Home Page',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        );
    }
  }
}

You can find complete project here Navigation Drawer with Multiple Fragments using bloc

Scamper answered 20/7, 2020 at 16:33 Comment(1)
I try to understand this... to me, it looks more like you're using a Drawer like a TabView rather than really navigating to new pages - or am I wrong? apart from that, I like that solution a lot, because that's exactly what I need! :DDomineer
Q
0

You can create the ScaffoldCustom when you ensure that all pages have only the body differently. But I feel that this approach is too restrictive. So, I am using this.

For the AppBar:

class AppBarPattern1 extends StatelessWidget implements PreferredSizeWidget {
  const AppBarPattern1({Key? key}) : super(key: key);

  @override
  // TODO: implement preferredSize
  Size get preferredSize => const Size.fromHeight(kToolbarHeight); // You can change it.
  /*
  /// The height of the toolbar component of the [AppBar].
  const double kToolbarHeight = 56.0;
  */

  @override
  Widget build(BuildContext context) {
    return AppBar();
  }
}

For the Drawer:

class DrawerPattern1 extends StatelessWidget {
  const DrawerPattern1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Drawer();
  }
}

Using like this:

class ExamplePage extends StatelessWidget {
  const ExamplePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const AppBarPattern1(),
      endDrawer: const DrawerPattern1(),
      body: SafeArea(child: Container()),
    );
  }
}

As you see these custom widgets can be const.

Quirita answered 14/12, 2022 at 13:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.