28

Is it possible to detect when a Drawer is open so that we can run some routine to update its content?

A typical use case I have would be to display the number of followers, likers... and for this, I would need to poll the server to get this information, then to display it.

I tried to implement a NavigatorObserver to catch the moment when the Drawer is made visible/hidden but the NavigatorObserver does not detect anything about the Drawer.

Here is the code linked to the NavigatorObserver:

import 'package:flutter/material.dart';

typedef void OnObservation(Route<dynamic> route, Route<dynamic> previousRoute);
typedef void OnStartGesture();

class NavigationObserver extends NavigatorObserver {
  OnObservation onPushed;
  OnObservation onPopped;
  OnObservation onRemoved;
  OnObservation onReplaced;
  OnStartGesture onStartGesture;

  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
    if (onPushed != null) {
      onPushed(route, previousRoute);
    }
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
    if (onPopped != null) {
      onPopped(route, previousRoute);
    }
  }

  @override
  void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
    if (onRemoved != null)
      onRemoved(route, previousRoute);
  }

  @override
  void didReplace({ Route<dynamic> oldRoute, Route<dynamic> newRoute }) {
    if (onReplaced != null)
      onReplaced(newRoute, oldRoute);
  }

  @override
  void didStartUserGesture() { 
    if (onStartGesture != null){
      onStartGesture();
    }
  }
}

and the initialization of this observer

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final NavigationObserver _observer = new NavigationObserver()
                                              ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
                                                print('** pushed route: $route');
                                              }
                                              ..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) {
                                                print('** poped route: $route');
                                              }
                                              ..onReplaced = (Route<dynamic> route, Route<dynamic> previousRoute) {
                                                print('** replaced route: $route');
                                              }
                                              ..onStartGesture = () {
                                                print('** on start gesture');
                                              };

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Title',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SplashScreen(),
        routes: <String, WidgetBuilder> {
          '/splashscreen': (BuildContext context) => new SplashScreen(),
        },
        navigatorObservers: <NavigationObserver>[_observer],
    );
  }
}

Thanks for your help.

boeledi
  • 6,837
  • 10
  • 32
  • 42
  • 1
    NavigatorObserver is not the solution. Can you instead show how you use Drawer ? – Rémi Rousselet Apr 22 '18 at 11:38
  • Hi @RémiRousselet. There is nothing special on the way I use the Drawer, there is very little to say. I simply initialize the drawer at the Scaffold level. I am waiting for a solution to detect when the drawer is open to further elaborate. But as you mentioned that the NavigatorObserver was not the solution, have you anything else in mind ? – boeledi Apr 22 '18 at 12:20

9 Answers9

39

This answer is old now. Please see @dees91's answer.

Detecting & Running Functions When Drawer Is Opened / Closed

  • Run initState() when open drawer by any action.
  • Run dispose() when close drawer by any action.
class MyDrawer extends StatefulWidget {
    @override
    _MyDrawerState createState() => _MyDrawerState();
}

class _MyDrawerState extends State<MyDrawer> {

    @override
    void initState() {
        super.initState();
        print("open");
    }

    @override
    void dispose() {
        print("close");
        super.dispose();
    }

    @override
    Widget build(BuildContext context) {
        return Drawer(
            child: Column(
                children: <Widget>[
                    Text("test1"),
                    Text("test2"),
                    Text("test3"),
                ],
            ),
        );
    }
}

State Management Considerations

If you are altering state with these functions to rebuild drawer items, you may encounter the error: Unhandled Exception: setState() or markNeedsBuild() called during build.

This can be handled by using the following two functions in initState() source

Option 1

WidgetsBinding.instance.addPostFrameCallback((_){
  // Add Your Code here.
});

Option 2

SchedulerBinding.instance.addPostFrameCallback((_) {
  // add your code here.
});

Full Example of Option 1

@override
void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
        // Your Code Here
    });
}
pepen
  • 661
  • 7
  • 7
  • 4
    why is this not accepted? This accounts for both button presses and sliding the drawer open – Johnny Boy Dec 22 '18 at 14:22
  • 3
    This solution worked perfectly for me. It should've been accepted. – JakeSays Dec 06 '19 at 07:53
  • Even though this is a good fix, it's still a workaround. Check @dees91's answer for a proper solution: https://stackoverflow.com/a/66600590/3197387 – Sisir Aug 29 '22 at 11:19
36

As https://github.com/flutter/flutter/pull/67249 is already merged and published with Flutter 2.0 here is proper way to detect drawer open/close:

Scaffold(
      onDrawerChanged: (isOpened) {
        //todo what you need for left drawer
      },
      onEndDrawerChanged: (isOpened) {
        //todo what you need for right drawer
      },
)
dees91
  • 1,211
  • 13
  • 21
19

Best solution

ScaffoldState has a useful method isDrawerOpen which provides the status of open/close.

Example: Here on the back press, it first checks if the drawer is open, if yes then first it will close before exit.

/// create a key for the scaffold in order to access it later.
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

@override
Widget build(context) {
   return WillPopScope(
  child: Scaffold(
    // assign key (important)
    key: _scaffoldKey,
    drawer: SideNavigation(),
  onWillPop: () async {
    // drawer is open then first close it
    if (_scaffoldKey.currentState.isDrawerOpen) {
      Navigator.of(context).pop();
      return false;
    }
    // we can now close the app.
    return true;
  });}
Vivek Bansal
  • 1,301
  • 13
  • 21
8

I think one simple solution is to override the leading property of your AppBar so you can have access when the menu icon is pressed an run your API calls based on that.

Yet I may have misunderstood your question because with the use case you provided, you usually need to manage it in a way that you can listen to any change which will update the value automatically so I am not sure what are you trying to trigger when the drawer is open.

Anyway here is the example.

enter image description here

class DrawerExample extends StatefulWidget {
  @override
  _DrawerExampleState createState() => new _DrawerExampleState();
}

class _DrawerExampleState extends State<DrawerExample> {
  GlobalKey<ScaffoldState> _key = new GlobalKey<ScaffoldState>();
  int _counter =0;
  _handleDrawer(){
      _key.currentState.openDrawer();

           setState(() {
          ///DO MY API CALLS
          _counter++;
        });

  }
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      key: _key,
      appBar: new AppBar(
        title: new Text("Drawer Example"),
        centerTitle: true,
        leading: new IconButton(icon: new Icon(
          Icons.menu
        ),onPressed:_handleDrawer,),
      ),
      drawer: new Drawer(
        child: new Center(
          child: new Text(_counter.toString(),style: Theme.of(context).textTheme.display1,),
        ),
      ),
    );
  }
}
Shady Aziza
  • 50,824
  • 20
  • 115
  • 113
6

You can simply use onDrawerChanged for detecting if the drawer is opened or closed in the Scaffold widget.

Property :

{void Function(bool)? onDrawerChanged}
Type: void Function(bool)?
Optional callback that is called when the Scaffold.drawer is opened or closed.

Example :

@override Widget build(BuildContext context) {

return Scaffold(
  onDrawerChanged:(val){
    if(val){
      setState(() {
        //foo bar;
      });
    }else{
      setState(() {
        //foo bar;
      });
    }
},     
  drawer: Drawer(        
      child: Container(
      )
  ));

}

Vishal Agrawal
  • 274
  • 4
  • 8
4

Unfortunately, at the moment there is no readymade solution.

You can use the dirty hack for this: to observe the visible position of the Drawer.

For example, I used this approach to synchronise the animation of the icon on the button and the location of the Drawer box.

enter image description here

The code that solves this problem you can see below:

    import 'package:flutter/material.dart';
    import 'package:flutter/scheduler.dart';

    class DrawerListener extends StatefulWidget {
      final Widget child;
      final ValueChanged<FractionalOffset> onPositionChange;

      DrawerListener({
        @required this.child,
        this.onPositionChange,
      });

      @override
      _DrawerListenerState createState() => _DrawerListenerState();
    }

    class _DrawerListenerState extends State<DrawerListener> {
      GlobalKey _drawerKey = GlobalKey();
      int taskID;
      Offset currentOffset;

      @override
      void initState() {
        super.initState();
        _postTask();
      }

      _postTask() {
        taskID = SchedulerBinding.instance.scheduleFrameCallback((_) {
          if (widget.onPositionChange != null) {
            final RenderBox box = _drawerKey.currentContext?.findRenderObject();
            if (box != null) {
              Offset newOffset = box.globalToLocal(Offset.zero);
              if (newOffset != currentOffset) {
                currentOffset = newOffset;
                widget.onPositionChange(
                  FractionalOffset.fromOffsetAndRect(
                    currentOffset,
                    Rect.fromLTRB(0, 0, box.size.width, box.size.height),
                  ),
                );
              }
            }
          }

          _postTask();
        });
      }

      @override
      void dispose() {
        SchedulerBinding.instance.cancelFrameCallbackWithId(taskID);
        if (widget.onPositionChange != null) {
          widget.onPositionChange(FractionalOffset(1.0, 0));
        }
        super.dispose();
      }

      @override
      Widget build(BuildContext context) {
        return Container(
          key: _drawerKey,
          child: widget.child,
        );
      }
    }

If you are only interested in the final events of opening or closing the box, it is enough to call the callbacks in initState and dispose functions.

Yuriy Luchaninov
  • 1,136
  • 1
  • 11
  • 7
  • Exactly what I was looking for, thanks for sharing! Very useful if you need to sync the animation of a button with the drawer movement! – androidseb Mar 15 '21 at 01:04
  • 1
    There is a readymade solution with Flutter 2: https://stackoverflow.com/a/66600590/11120544 This answer is from this question itself – Apps 247 Apr 08 '21 at 07:57
0

there is isDrawerOpen property in ScaffoldState so you can check whenever you want to check.

create a global key ;

GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

assign it to scaffold

Scaffold(
      key: scaffoldKey,
      appBar: ..)

check where ever in the app

bool opened =scaffoldKey.currentState.isDrawerOpen;
Bilal Şimşek
  • 5,453
  • 2
  • 19
  • 33
0

By the time this question was being posted it was a bit trick to accomplish this. But from Flutter 2.0, it is pretty easy. Inside your Scaffold you can detect both the right drawer and the left drawer as follows.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      onDrawerChanged: (isOpened) {
        *//Left drawer, Your code here,*
      },
      onEndDrawerChanged: (isOpened) {
        *//Right drawer, Your code here,*
      },
    );
  }
Francisca Mkina
  • 502
  • 7
  • 15
0

You can use Scaffold.of(context) as below to detect the Drawer status :

NOTE: you must put your code in the Builder widget to use the context which contains scaffold.

Builder(
                builder: (context) => IconButton(
                  icon: Icon(
                    Icons.menu,
                    color: getColor(context, opacity.value),
                  ),
                  onPressed: () {
                    if (Scaffold.of(context).isDrawerOpen) {
                      Scaffold.of(context).closeDrawer();
                    } else {
                      Scaffold.of(context).openDrawer();
                    }
                  },
                ),
              ),
ParSa
  • 1,118
  • 1
  • 13
  • 17