I've got a MaterialApp which uses a builder with a scaffold in it. When I navigate from page to page the scaffold and app bar does not rebuild, but the body of the scaffold does:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:raven_front/pages/pages.dart';
Future<void> main() async {
runApp(RavenMobileApp());
}
class RavenMobileApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/splash',
routes: pages.routes(context),
builder: (context, child) {
return SafeArea(
child: Scaffold(
extendBodyBehindAppBar: false,
appBar: BackdropAppBar(), // pretty much regular app bar
body: child!, // pages of app
));
},
);
}
}
but when I'm on a page where I need to show a bottom modal sheet, or alert box, or anything with a scrim, it doesn't apply to the app bar:
for example, I might make the ModalBottomSheet this way
await showModalBottomSheet<void>(
context: context,
elevation: 1,
barrierColor: AppColors.black38, // not applied to app bar
shape: components.shape.topRounded,
builder: (BuildContext context) {
...
});
With my setup of having a builder in the MaterialApp, how can I get the scrim to cover everything?
I tried saving the context used in the MaterialApp (highest level) and using that in the modal sheet, but that errored saying that context doesn't have a Navigator. I'm hoping I can keep the current design but extend the scrim over the app bar somehow.
can you believe it, I had to roll my own. the other option was to abandon the builder material app design mentioned in the question. This is what I had to do:
app bar:
Stack(
children: [
appBar,
AppBarScrim(),
])
app bar scrim
class AppBarScrim extends StatefulWidget {
const AppBarScrim({Key? key}) : super(key: key);
#override
State<AppBarScrim> createState() => _AppBarScrimState();
}
class _AppBarScrimState extends State<AppBarScrim> {
late List listeners = [];
final Duration waitForSheetDrop = Duration(milliseconds: 50);
bool applyScrim = false;
#override
void initState() {
super.initState();
listeners.add(streams.app.scrim.listen((bool value) async {
if (applyScrim && !value) {
await Future.delayed(waitForSheetDrop);
setState(() {
applyScrim = value;
});
}
if (!applyScrim && value) {
setState(() {
applyScrim = value;
});
}
}));
}
#override
void dispose() {
for (var listener in listeners) {
listener.cancel();
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
Navigator.of(components.navigator.routeContext!).pop();
streams.app.scrim.add(false);
},
child: AnimatedContainer(
duration: waitForSheetDrop,
color: applyScrim ? Colors.black38 : Colors.transparent,
height: applyScrim ? 56 : 0,
));
}
}
showDialog( // and show bottom modal sheet...
..
builder: (BuildContext context) {
streams.app.scrim.add(true); // trigger
return AlertDialog(...);
}).then((value) => streams.app.scrim.add(false)); // remove trigger
Related
The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}
I am new to Flutter and trying to trigger a snack bar on page load if a message was returned from the page I navigated from. I have managed to get the message to display on a button click, but get an error stating that my context does not have a Scaffold if I try to do it elsewhere.
I am also struggling to find an example of how to show a sack bar without user interaction, so if anyone has a reference, that would surely go a long way in helping as well.
Here is a simplified version of my view:
class LandingView extends StatefulWidget {
final LandingViewModel viewModel;
LandingView(this.viewModel);
#override
State<StatefulWidget> createState() {
return new _ViewState();
}
}
class _ViewState extends State<LandingView> {
#override
void initState() {
super.initState();
}
void _showSnackbar(context, message) {
final snackBar = SnackBar(
content: Text(message),
);
Scaffold.of(context).showSnackBar(snackBar);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: new GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(new FocusNode());
},
child: _buildLayout(context),
),
),
);
}
Widget _buildLayout(BuildContext context) {
Map<String, dynamic> args = getArgs(context); //get value from previous page
if (args != null &&
args["Toast Message"] != null) //check if a value was returned from the previous page. This has been tested and a valid string is being returned
_showSnackbar(
context, args["Toast Message"]); //if so call snack bar function
// this throws an error saying "Scaffold.of() called with a context that does not contain a Scaffold"
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: boxConstraints.maxHeight),
child: RaisedButton(
child: Text(
"Show Snack Bar",
),
onPressed:
() {
if (args != null &&
args["Toast Message"] !=
null) //check if a value was returned from the previous page. This has been tested and a valid string is being returned
_showSnackbar(context,
args["Toast Message"]); //if so call snack bar function
//this works perfectly
}),
),
);
});
}
}
Any advice would be greatly appreciated
You're getting that because your LandingView widget is not in a Scaffold. You can fix this by putting the LandingView widget inside a StatelessWidget with a Scaffold and changing any references to LandingView to LandingViewPage:
class LandingViewPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: LandingView()
);
}
}
We can do this with addPostFrameCallback method
#override
void initState(){
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => scaffold.showSnackBar(SnackBar(content: Text("snackbar")));
}
In a stateful widget put:
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Error")));
});
}
I'm trying to show a splash screen on initial app startup until I have all of the data properly retrieved. As soon as it's there, I want to navigate to the main screen of the app.
Unfortunately, I can't find a good way to trigger a method that runs that kind of Navigation.
This is the code that I'm using to test this idea. Specifically, I want to run the command Navigator.pushNamed(context, 'home'); when the variable shouldProceed becomes true. Right now, the only way I can think to do it is to display a button that I need to press to trigger the navigation code:
import 'package:flutter/material.dart';
import 'package:catalogo/src/navigationPage.dart';
class RouteSplash extends StatefulWidget {
#override
_RouteSplashState createState() => _RouteSplashState();
}
class _RouteSplashState extends State<RouteSplash> {
ValueNotifier<bool> buttonTrigger;
bool shouldProceed = false;
_fetchPrefs() async { //this simulates the asynchronous function
await Future.delayed(Duration(
seconds:
1)); // dummy code showing the wait period while getting the preferences
setState(() {
shouldProceed = true; //got the prefs; ready to navigate to next page.
});
}
#override
void initState() {
super.initState();
_fetchPrefs(); // getting prefs etc.
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: shouldProceed
? RaisedButton(
onPressed: () {
print("entered Main");
Navigator.pushNamed(context, 'home'); // <----- I want this to be triggered by shouldProceed directly
},
child: Text("Continue"),
)
: CircularProgressIndicator(), //show splash screen here instead of progress indicator
),
);
}
}
So, in short, how can I trigger a function that runs the Navigation code when shouldProceed changes?
All you have to do is after you get the preferences, just navigate to the screen and have the build method just build a progress indicator.
Try this:
_fetchPrefs() async {
await Future.delayed(Duration(seconds: 1));
Navigator.of(context).pushNamed("home"); //stateful widgets give you context
}
Here's your new build method:
#override
Widget build(BuildContext context) {
return Center(child: CircularProgressIndicator());
}
I've made a DartPad to illustrate: https://dartpad.dartlang.org/431fcd9a1ea5748a82506f13be042e85
Create a widget which can be shown or hidden similar to my ProgressBar code. Show and hide based on a timer or when the data load from the api has completed.
ProgressBar progressBar = new ProgressBar();
progressBar.show(context);
Provider.of<Api>(context, listen: false)
.loadData(context, param)
.then((data) {
progressBar.hide();
Provider.of<Api>(context, listen: false).dataCache.login = loginValue;
Navigator.pop(context, true);
Navigator.pushNamed(context, "/home");
}
});
replace the ProgressBar code with your splash screen:
class ProgressBar {
OverlayEntry _progressOverlayEntry;
void show(BuildContext context){
_progressOverlayEntry = _createdProgressEntry(context);
Overlay.of(context).insert(_progressOverlayEntry);
}
void hide(){
if(_progressOverlayEntry != null){
_progressOverlayEntry.remove();
_progressOverlayEntry = null;
}
}
OverlayEntry _createdProgressEntry(BuildContext context) =>
OverlayEntry(
builder: (BuildContext context) =>
Stack(
children: <Widget>[
Container(
color: Colors.white.withOpacity(0.6),
),
Positioned(
top: screenHeight(context) / 2,
left: screenWidth(context) / 2,
child: CircularProgressIndicator(),
)
],
)
);
double screenHeight(BuildContext context) =>
MediaQuery.of(context).size.height;
double screenWidth(BuildContext context) =>
MediaQuery.of(context).size.width;
}
I have an intro screen for my app, but it shows every time I open the app,
I need to show that for the 1st time only.
How to do that?
//THIS IS THE SCREEN COMES 1ST WHEN OPENING THE APP (SPLASHSCREEN)
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
//After 2seconds of time the Introscreen will e opened by bellow code
Timer(Duration(seconds: 2), () => MyNavigator.goToIntroscreen(context));
}
//The below code has the text to show for the spalshing screen
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Center(
child: Text('SPLASH SCREEN'),
),
);
}
}
Every time this screen opens the intro screen with 2 seconds delay.
but I want for the first time only How to do that with sharedpreference??
Please add the required code.
If you wish to show the intro screen only for the first time, you will need to save locally that this user has already seen intro.
For such thing you may use Shared Preference. There is a flutter package for Shared Preference which you can use
EDITED:
Please refer to the below complete tested code to understand how to use it:
import 'dart:async';
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
color: Colors.blue,
home: new Splash(),
);
}
}
class Splash extends StatefulWidget {
#override
SplashState createState() => new SplashState();
}
class SplashState extends State<Splash> with AfterLayoutMixin<Splash> {
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _seen = (prefs.getBool('seen') ?? false);
if (_seen) {
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (context) => new Home()));
} else {
await prefs.setBool('seen', true);
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (context) => new IntroScreen()));
}
}
#override
void afterFirstLayout(BuildContext context) => checkFirstSeen();
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Text('Loading...'),
),
);
}
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Hello'),
),
body: new Center(
child: new Text('This is the second page'),
),
);
}
}
class IntroScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('IntroScreen'),
),
body: new Center(
child: new Text('This is the IntroScreen'),
),
);
}
}
Thanks to Ben B for noticing the incorrect use of delay in initState. I had used a delay because sometimes the context is not ready immediately inside initState.
So now I have replaced that with afterFirstLayout which is ready with the context. You will need to install the package after_layout.
I was able to do without using after_layout package and Mixins and instead I have used FutureBuilder.
class SplashState extends State<Splash> {
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _seen = (prefs.getBool('seen') ?? false);
if (_seen) {
return HomeScreen.id;
} else {
// Set the flag to true at the end of onboarding screen if everything is successfull and so I am commenting it out
// await prefs.setBool('seen', true);
return IntroScreen.id;
}
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: checkFirstSeen(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return MaterialApp(
initialRoute: snapshot.data,
routes: {
IntroScreen.id: (context) => IntroScreen(),
HomeScreen.id: (context) => HomeScreen(),
},
);
}
});
}
}
class HomeScreen extends StatelessWidget {
static String id = 'HomeScreen';
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Hello'),
),
body: new Center(
child: new Text('This is the second page'),
),
);
}
}
class IntroScreen extends StatelessWidget {
static String id = 'IntroScreen';
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('IntroScreen'),
),
body: new Center(
child: new Text('This is the IntroScreen'),
),
);
}
}
I always try to use minimum count of packages, because in future it can conflict with ios or android. So my simple solution without any package:
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
final splashDelay = 2;
#override
void initState() {
super.initState();
_loadWidget();
}
_loadWidget() async {
var _duration = Duration(seconds: splashDelay);
return Timer(_duration, checkFirstSeen);
}
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _introSeen = (prefs.getBool('intro_seen') ?? false);
Navigator.pop(context);
if (_introSeen) {
Navigator.pushNamed(context, Routing.HomeViewRoute);
} else {
await prefs.setBool('intro_seen', true);
Navigator.pushNamed(context, Routing.IntroViewRoute);
}
}
#override
Widget build(BuildContext context) {
//your splash screen code
}
}
Use shared_preferences:
Full code:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
var prefs = await SharedPreferences.getInstance();
var boolKey = 'isFirstTime';
var isFirstTime = prefs.getBool(boolKey) ?? true;
runApp(MaterialApp(home: isFirstTime ? IntroScreen(prefs, boolKey) : RegularScreen()));
}
class IntroScreen extends StatelessWidget {
final SharedPreferences prefs;
final String boolKey;
IntroScreen(this.prefs, this.boolKey);
Widget build(BuildContext context) {
prefs.setBool(boolKey, false); // You might want to save this on a callback.
return Scaffold();
}
}
class RegularScreen extends StatelessWidget {
Widget build(BuildContext context) => Scaffold();
}
I just had to do exactly the same thing, here's how I did it:
First, in my main method, I open the normal main page and the tutorial:
MaterialApp(
title: 'myApp',
onGenerateInitialRoutes: (_) => [MaterialPageRoute(builder: mainPageRoute), MaterialPageRoute(builder: tutorialSliderRoute)],
)
...and then I use a FutureBuilder to build the tutorial only if necessary:
var tutorialSliderRoute = (context) => FutureBuilder(
future: Provider.of<UserConfiguration>(context, listen: false).loadShowTutorial() // does a lookup using Shared Preferences
.timeout(Duration(seconds: 3), onTimeout: () => false),
initialData: null,
builder: (context, snapshot){
if (snapshot.data == null){
return CircularProgressIndicator(); // This is displayed for up to 3 seconds, in case data loading doesn't return for some reason...
} else if (snapshot.data == true){
return TutorialSlider(); // The Tutorial, implemented using IntroSlider()
} else {
// In case the tutorial shouldn't be shown, just return an empty Container and immediately pop it again so that the app's main page becomes visible.
SchedulerBinding.instance.addPostFrameCallback((_){Navigator.of(context).pop();});
return Container(width: 0, height: 0);
}
},
);
Also, I think the tutorial should be shown again in case the user does not finish it, so I set only set the variable showTutorial to false once the user has completed (or skipped) the tutorial:
class TutorialSlider extends StatefulWidget {
#override
State<StatefulWidget> createState() => TutorialSliderState();
}
class TutorialSliderState extends State<TutorialSlider> {
...
#override
Widget build(BuildContext context) => IntroSlider(
...
onDonePress: (){
Provider.of<UserConfiguration>(context, listen: false).setShowTutorial(false);
Navigator.of(context).pop();
}
);
}
I took a different approach. I agree with the other answers that you should save your isFirstRun status via SharedPreferences. The tricky part then is how to show the correct widget in such a way that when you hit back you close out of the app correctly, etc. I first tried doing this by launching a my SplashWidget while building my HomePageWidget, but this turned out to lead to some weird Navigator errors.
Instead, I wound up calling runApp() multiple times with my different widget as appropriate. When I need to close the SplashWidget, rather than pop it, I just call runApp() again, this time with my HomePageWidget as the child property. It is safe to call runApp() multiple times according to this issue, indeed even for splash screens.
So it looks something like this (simplified obviously):
Future<void> main() async {
bool needsFirstRun = await retrieveNeedsFirstRunFromPrefs();
if (needsFirstRun) {
// This is will probably be an async method but no need to
// delay the first widget.
saveFirstRunSeen();
runApp(child: SplashScreenWidget(isFirstRun: true));
} else {
runApp(child: HomePageWidget());
}
}
I have an isFirstRun property on SplashScreenWidget because I can launch it in two ways--once as a true splash screen, and once from settings so that users can see it again if they want. I then inspect that in SplashScreenWidget to determine how I should return to the app.
class SplashScreenWidget extends StatefulWidget {
final bool isFirstRun;
// <snip> the constructor and getState()
}
class _SplashScreenWidgetState extends State<SplashScreenWidget> {
// This is invoked either by a 'skip' button or by completing the
// splash screen experience. If they just hit back, they'll be
// kicked out of the app (which seems like the correct behavior
// to me), but if you wanted to prevent that you could build a
// WillPopScope widget that instead launches the home screen if
// you want to make sure they always see it.
void dismissSplashScreen(BuildContext ctx) {
if (widget.isFirstRun) {
// Then we can't just Navigator.pop, because that will leave
// the user with nothing to go back to. Instead, we will
// call runApp() again, setting the base app widget to be
// our home screen.
runApp(child: HomePageWidget());
} else {
// It was launched via a MaterialRoute elsewhere in the
// app. We want the dismissal to just return them to where
// they were before.
Navigator.of(ctx).pop();
}
}
}
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.
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
});
}
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
},
)
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;
});}
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.
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,),
),
),
);
}
}
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(
)
));
}
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.
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.
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;
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,*
},
);
}
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();
}
},
),
),