How to detect the drawer is closed on flutter?

Viewed 4718

As title. It since that we can detect the drawer is opened, but is this possible to check it is closed or not? Thanks.

6 Answers

I have added this feature in Flutter 2.0.0. Make sure you are using Flutter SDK version >= 2.0.0 to use this.

Simply use a callback in Scaffold

return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      drawer: NavDrawer(),
      onDrawerChanged: (isOpen) {
        // write your callback implementation here
        print('drawer callback isOpen=$isOpen');
      },
      endDrawer: NavDrawerEnd(),
      onEndDrawerChanged: (isOpen) {
        // write your callback implementation here
        print('end drawer callback isOpen=$isOpen');
      },
      body:
      ...

Pull request merged in 2.0.0: https://github.com/flutter/flutter/pull/67249

Happy coding!

Declare a GlobalKey to reference your drawer:

GlobalKey _drawerKey = GlobalKey();

Put the key in your Drawer:

 drawer: Drawer(
                  key: _drawerKey,

Check if your drawer is visible:

 final RenderBox box = _drawerKey.currentContext?.findRenderObject();
 if (box != null){
    //is visible
 } else {
   //not visible  
} 

You can copy paste run full code below
You can wrap Drawer with a StatefulWidget and put callback in initState() and dispose()
initState() will call widget.callback(true); means open
dispose() will call widget.callback(false); means close
Slide also work in this case

code snippet

drawer: CustomDrawer(
        callback: (isOpen) {
          print("isOpen ${isOpen}");
          WidgetsBinding.instance.addPostFrameCallback((_) {
            setState(() {
              _isDrawerOpen = isOpen;
            });
          });
        },
...
class CustomDrawer extends StatefulWidget {
  CustomDrawer({
    Key key,
    this.elevation = 16.0,
    this.child,
    this.semanticLabel,
    this.callback,
  })  : assert(elevation != null && elevation >= 0.0),
        super(key: key);

  final double elevation;
  final Widget child;
  final String semanticLabel;

  final DrawerCallback callback;
  @override
  _CustomDrawerState createState() => _CustomDrawerState();
}

class _CustomDrawerState extends State<CustomDrawer> {
  @override
  void initState() {
    if (widget.callback != null) {
      widget.callback(true);
    }

    super.initState();
  }

  @override
  void dispose() {
    if (widget.callback != null) {
      widget.callback(false);
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Drawer(
        key: widget.key,
        elevation: widget.elevation,
        semanticLabel: widget.semanticLabel,
        child: widget.child);
  }
}

working demo

enter image description here

full code

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  bool _isDrawerOpen = false;
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      drawer: CustomDrawer(
        callback: (isOpen) {
          print("isOpen ${isOpen}");
          WidgetsBinding.instance.addPostFrameCallback((_) {
            setState(() {
              _isDrawerOpen = isOpen;
            });
          });
        },
        child: ListView(
          padding: EdgeInsets.zero,
          children: <Widget>[
            DrawerHeader(
              child: Text('Drawer Header'),
              decoration: BoxDecoration(
                color: Colors.blue,
              ),
            ),
            ListTile(
              title: Text('Item 1'),
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => SecondRoute()),
                );
              },
            ),
            ListTile(
              title: Text('Item 2'),
              onTap: () {
                // Update the state of the app.
                // ...
              },
            ),
          ],
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Align(
              alignment: Alignment.centerRight,
              child: Text(
                _isDrawerOpen.toString(),
              ),
            ),
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class CustomDrawer extends StatefulWidget {
  CustomDrawer({
    Key key,
    this.elevation = 16.0,
    this.child,
    this.semanticLabel,
    this.callback,
  })  : assert(elevation != null && elevation >= 0.0),
        super(key: key);

  final double elevation;
  final Widget child;
  final String semanticLabel;

  final DrawerCallback callback;
  @override
  _CustomDrawerState createState() => _CustomDrawerState();
}

class _CustomDrawerState extends State<CustomDrawer> {
  @override
  void initState() {
    if (widget.callback != null) {
      widget.callback(true);
    }

    super.initState();
  }

  @override
  void dispose() {
    if (widget.callback != null) {
      widget.callback(false);
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Drawer(
        key: widget.key,
        elevation: widget.elevation,
        semanticLabel: widget.semanticLabel,
        child: widget.child);
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("route test"),
        ),
        body: Text("SecondRoute"));
  }
}

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(
      )
  ));
}

When you click a Drawer Item where you will navigate to a new screen, there in the Navigator.push(..) call, you can add a .then(..) clause, and then know when the Drawer item Screen has been popped.

Here is the ListTile for a Drawer item which makes the Navigator.push(..) call when clicked , and the the associated .then(..) callback block:

     ListTile(
          title: Text('About App'),
          onTap: () {
            Navigator.push(
              _ctxt,
              MaterialPageRoute(builder: (context) => AboutScreen()),
            ).then(
              (value) {
                print('Drawer callback for About selection');
                if (_onReadyCallback != null) {
                  _onReadyCallback();
                }
              },
            );
          }),

_onReadyCallback() represents a Function param you can pass in.

I found this is approach - of leveraging the .then() callback from a .push() call - to be a very useful concept to understand with Flutter in general.

Big thanks to the main 2 answers here: Force Flutter navigator to reload state when popping

Here's the complete Drawer code:

Drawer drawer = Drawer(
  child: ListView(
    padding: EdgeInsets.zero,
    children: <Widget>[
      DrawerHeader(
        decoration: BoxDecoration(
          color: Color(0xFF7FAD5F),
        ),
        child: Text(App.NAME_MENU),
      ),
      ListTile(
          title: Text('About App'),
          onTap: () {
            Navigator.push(
              _ctxt,
              MaterialPageRoute(builder: (context) => AboutScreen()),
            ).then(
              (value) {
                print('Drawer callback for About selection');
                if (_onReadyCallback != null) {
                  _onReadyCallback();
                }
              },
            );
          }),
    ],
  ),
);

I would recommend that you use this package : https://pub.dev/packages/visibility_detector.

Afterwards you should assign a GlobalKey, like _drawerKey for instance, to the Drawer widget, after which you would be able to detect when the drawer is closed like this:

VisibilityDetector(
              key: _drawerKey,
              child: Container(),
              onVisibilityChanged: (info) {
                if (info.visibleFraction == 0.0) {
                  // drawer not visible.
                }
              },
            )
Related