flutter - android back button does not call onWillPop of WillPopScope - flutter

I would like to exit my application. I have implemented a WillPopScope, but it looks like the onWillPop function is not being called at all. I tried many things like swap WillPopScope with Scaffold, changing the return value of the function, but it just looks like it is not working as expected.
My code:
Future<bool> _willPopCallback() async {
exit(0);
// await showDialog or Show add banners or whatever
// then
return true; // return true if the route to be popped
}
return Scaffold(
appBar: MyAppBar(
leading: DrawerAction(),
title: Text(AppLocalizations.of(context).textCapitalized('home_page')),
onSearch: (searchTerms) => this.search(searchTerms, context),
),
body: new WillPopScope(
onWillPop: _willPopCallback, // Empty Function.
child: //my screen widgets
I am not sure if this is a bug I should report to flutter or I am doing something wrong. Happy to provide more code on request.
I have tried:
exit(0);
Navigator.of(context).pop();
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
Thanks in advance!

I managed to solve my problem and I think it is a very particular case, but it still might be helpful to someone.
TL;DR: Ensure that you dont have multiple Scaffolds in your widgets
I was using IndexedStack in my menu navigator, obviously wrapped with a Scaffold. The pages of the stack had Scaffold as well, and with this combination WillPopScope was not working neither in the navigator page neither in its stack pages. I solved by removing all the Scaffolds in the stack pages and having only one in the controller. In this way I managed to use WillPopScope correctly.

First of all do not ever use exit(0). It may be fine in Android environment, but apple won't allow the app on app store if it programmatically shuts down itself.
Here in the docs of onWillPop it clearly mentions that function should resolves to a boolean value.
Future<bool> _willPopCallback() async {
// await showDialog or Show add banners or whatever
// then
return Future.value(true);
}
This only works if your current page is the root of navigation stack.

Modify the code to return WillPopScope, and have Scaffold as a child.
return new WillPopScope(
onWillPop: _willPopCallback,
child: new Scaffold(
//then the rest of your code...

i know i am too late, but the problem still exists.
maybe i found the right solution.
make sure you are passing MaterialApp to the runApp method like this:
runApp(MaterialApp(home: MyFirstPage()));
this works for me for all my application's widgets. if you do not want to use it just wrap your widget in MaterialApp but do not forget that in every MaterialApp instance a new Navigator is created, so for me i just created one as above and in all my pages i just used scaffold and everything is ok.

I also stuck in the same problem but after a lot of searching, I found that this error is related to my parent container.
return WillPopScope(
onWillPop: () => _onWillPop(),
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
...
],
),

Another possible reason: my implementation of _onWillPop() was throwing an exception and the code inside _onWillPop() was ignored. The exception did not appear in the log.
I resolved it by using a TRY/CATCH inside _onWillPop(), and handling all code paths.

I have been battling this and initially thought it had something to do with the nested Scaffold widgets as the OP had mentioned in their answer above. I tested this though and still had the same problem. The answer for me was that my root Scaffold was a child of a Navigator. It worked as soon as I removed the Scaffold as a child of the Navigator. Thankfully I didn't need a Navigator at the root level anyway as I was using an IndexedStack which has multiple Navigator widgets in it.

This is a late answer but I hope can helps someone.
The #Gicminos answer was right. If you have nested scaffold willPopScope not worked.
I wanna add some info in case you need.
I have a Scaffold containing bottomNavBar. Every Item in bottomNav is a Navigator which children are Scaffold (you notice that in this moment there are scaffolds innested).
This is my MainScaffold containing the bottom bar:
...
_navigatorKeys = {
TabItem.tabOne: tabOneKey,
TabItem.tabTwo: GlobalKey<NavigatorState>(),
};
...
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
//check if there are pages in stack so it can pop;
var navigatorState =(_navigatorKeys.values.toList([_selectedIndex]as GlobalKey<NavigatorState>).currentState;
if ( navigatorState !=null) {
if (!await navigatorState
.maybePop()) return true;
}
return false;
},
child: Scaffold(
body: SafeArea(
child: IndexedStack(
index: _selectedIndex,
children: _pages,
),
),
resizeToAvoidBottomInset: true,
bottomNavigationBar: Container(...)
...
}
If you wrap with a WillPopScope widget also your children like in the code below:
#override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context)!;
return WillPopScope(
onWillPop: () async {
debugPrint("test");
return true;
},
child: Scaffold(...)
}
both onWillPop will be called (in main scaffold and children scaffold).
In the example the first one will pop only if can (there are page in navigator stack), the second one will be called immediatly after the first one and it will call the debugPrint function before returned

I my case onWillPop didn't call because I had a custom AppBar and tried to call Navigator.pop(context) instead of Navigator.maybePop(context).

Related

When do we initialise a provider in flutter?

I just arrived on a flutter project for a web app, and all developers have a problem using flutter provider for state management.
What is the problem
When you arrive on a screen, the variables of the corresponding provider are initialised by calling a function of the provider. This function calls an api, and sets the variables in the provider.
Problem : This function is called in the build section of the widget. Each time the window is resized, the widget is rebuilt, and the function is called again.
What we want
We want to call an api when the page is first displayed, set variables with the result, and not call the api again when the widget is rebuilt.
What solution ?
We use a push from the first screen to go to the second one. We can call the function of the provider at this moment, to initialise the provider just before the second screen.
→ But a refresh on the second page will clear the provider variables, and the function to initialise them will not be called again.
We call the function to initialise the provider in the constructor of the second screen. Is it a good pattern ?
Thank you for your help in my new experience with flutter :)
I think you're mixing a couple different issues here:
How do you correctly initialize a provider
How do you call a method on initialization (only once)
For the first question:
In your main.dart file you want to do something like this:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => SomeProvider()),
ChangeNotifierProvider(create: (context) => AnotherProvider()),
],
child: YourRootWidget();
);
}
Then in a widget (that probably represents a "screen" in your app), you need to do something like this to consume state changes from that provider:
#override
Widget build(BuildContext context) {
return Container(
child: Consumer<SomeProvider>(
builder: (context, provider, child) {
return Text(provider.someState);
}
),
)
}
And you need to do something like this to get access to the provider to mutate state:
#override
Widget build(BuildContext context) {
SomeProvider someProvider = Provider.of<SomeProvider>(context, listen: false);
return Container(
child: TextButton(
child: Text('Tap me'),
onPressed: () async {
await someProvider.mutateSomeState();
}
),
)
}
Regarding the second question... You can (I think) just use the initState() method on a widget to make the call only 1 time. So...
#override
void initState() {
super.initState();
AnotherProvider anotherProvider = Provider.of<AnotherProvider>(context, listen: false);
Future.microtask(() {
anotherProvider.doSomethingElse();
});
}
If I'm off on any of that, I'm sorry. That mirrors my implementation and works fine/well.
A caveat here is that I think RiverPod is likely the place you really want to go (it's maybe easier to work with and has additional features that are helpful, etc.) but I've not migrated to RiverPod yet and do not have that figured out all the way.
Anyway... Good luck!
As far as I understood, you can wrap your application with MultiProvider and call the API before going to the second screen.

My flutter android back button event is not triggering using willpopscope

Help! my will pop not even printing. Here is the code:
Future<bool> _willPopCallback() async {
print('Hi');
return false;
}
WillPopScope(
onWillPop: _willPopCallback,
child: Scaffold()
It was not working i tried my best so at the end i started using a package called back_button_interceptor. Its easy to use you can find here:
https://pub.dev/packages/back_button_interceptor/example

Flutter Bloc , Bloc state , navigate?

what I’m facing now is after I implemented bloc following one of the tutorials, I'm stuck now in place where after I'm getting the response and the state is changed, I want to navigate to another widget
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(APP_TITLE),
),
body: buildBody(context));
}
}
BlocProvider<SignInBloc> buildBody(BuildContext context) {
return BlocProvider(
create: (_) => sl<SignInBloc>(),
child: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: <Widget>[
BlocBuilder<SignInBloc, SignInState>(
builder: (context, state) {
if(state is Empty)
return MessageDisplay(message: 'Sign In please.',);
else if(state is Loaded)
return HomePage();
else
return MessageDisplay(message: 'Sign In please.',);
}
),
SignInControls(),
],
),
),
),
);
}
in state of loaded I want to navigate to another widget.
so how to achieve that, and what is the best way for it?
You can't use the navigator or change the state while the widget is being built (your case).
There're two ways
1. The old fashioned way
WidgetsBinding.instance.addPostFrameCallback((_){
// Your code goes here
});
2. Since you already implemented the BLOC library you have a more elegant way to achieve this by using BlocListener. you can learn more about it in the documentation
Hope i helped!
Navigation can be used like Inherited widgets:
Navigator nav = Navigator.of(this.context);
then you can use somthing like:
nav.push(MaterialPageRoute(builder: (context) => YourSecondPage()))
in flutter, you can't just move to some page directly. you should use a route.
I think the cleanest way to use named routes. this is an example:
// here you put a class of names to use later in all of your project.
class RouteNames{
static String homepage = "/";
static String otherPage= "/otherpage";
}
// in your main file , MyApp class
var routes = {
RouteNames.homepage: (context)=> new MyHomePage(),
RouteNames.otherPage: (context)=> new MyOtherPage()
};
// then use routes variable in your MaterialApp constructor
// and later on in your project you can use this syntax:
Navigator.of(context).pushNamed(RouteNames.otherPage);
I think this way is clean and it's centralized, it's good if you want to send arguments to routes.
To learn more about navigation: navigation official documentation is pretty good
A note about the Bloc builder & listener:
Since BlocBuilder is going to be called lots of times. it should only contain widgets and widgets only. if you put navigation code inside it, this code would be called multiple times.
As Ayham Orfali said You definitely should use BlocListener for that. Inside it you can listen to changes in state. here is an example
// some code
children: <Widget>[
BlocListener(
bloc: BlocProvider.of<SignInBloc>(context),
listener: (context, state) {
if(state is Loaded){
Navigator.of(context).pushNamed("some other page");
}
// else do nothing!
},
child:// just bloc builder which contains widgets only. ,
SignInControls(),
]
// some other code

Use showDialog with conditional in build method

I have a screen, which shows a button. If I press it, an async job is started. During this time, I want to show an AlertDialog with a spinning wheel. If that job is finished, i will dismiss the dialog or show some errors. Here is my (simplified) code:
Widget build(BuildContext context) {
if (otherClass.isTaskRunning()) {
showDialog( ... ); // Show spinning wheel
}
if (otherClass.hasErrors()) {
showDialog( ...); // Show errors
}
return Scaffold(
...
FlatButton(
onPress: otherClass.startJob
)
);
}
The build will be triggered when the job status is changed or if there are errors. So far, so good, but if I run this code, I got this error message:
Exception has occurred. FlutterError (setState() or markNeedsBuild()
called during build. This Overlay widget cannot be marked as needing
to build because the framework is already in the process of building
widgets. A widget can be marked as needing to be built during the
build phase only if one of its ancestors is currently building. This
exception is allowed because the framework builds parent widgets
before children, which means a dirty descendant will always be built.
Otherwise, the framework might not visit this widget during this build
phase. The widget on which setState() or markNeedsBuild() was called
was: Overlay-[LabeledGlobalKey#357d8] The widget which
was currently being built when the offending call was made was:
SettingsScreen)
So, the repaint of the screen will be overlap somehow. I am not sure how to fix this. It feels like I am using this completely wrong. What is the prefered way to handle this kind of interaction (trigger "long" running task, show progress indicator and possible errors)?
The problem is the dialog is going to show while the build method hasn't already finish. So if you want to show a Dialog, you should do it after the build method has finished. To do that, you can use this: WidgetsBinding.instance.addPostFrameCallback(), that will call a function after the last frame was built (just after build method ends).
Other thing you can do is using the ternary operator to show a loading widget like so:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: otherClass.isTaskRunning()
? CircularProgressIndicator()
: FlatButton(
onPressed: () {},
),
);
}
As being said in the comment, calling showDialog and similar inside build is anti-pattern. Here is the detailed explanation.
Although is a bit late, it is important to note showDialog is indeed a async method, returning at the time that the dialog dismisses, and build is a sync in nature. Under the hood, it use Navigator.of(context).push.
Referring to this question,
The build method is designed in such a way that it should be pure/without side effects. This is because many external factors can trigger a new widget build, such as: Route pop/push
So this directly caused flutter to complain setState is called during build. You could just use a FutureBuilder inside the dialog.
One thing is clear is that your SettingsScreen is not required to fetch anything and so you should not brother with any of them. As you are deferring to do that as the button fires, then you should do it within dialog.
Widget build(BuildContext context) {
return Scaffold(
body: FlatButton(
onPress: () async {
await showDialog(
context: context,
builder: (BuildContext context) {
return FutureBuilder(
future: otherClass.startJob(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) // Show error
if (snapshot.hasData) // Show data, or you can just close it by Navigator.pop
else // show spinning wheel
}
}
);
}
);
}
)
);
}

InheritedWidget with Scaffold as child doesn't seem to be working

I was hoping to use InheritedWidget at the root level of my Flutter application to ensure that an authenticated user's details are available to all child widgets. Essentially making the Scaffold the child of the IW like this:
#override
Widget build(BuildContext context) {
return new AuthenticatedWidget(
user: _user,
child: new Scaffold(
appBar: new AppBar(
title: 'My App',
),
body: new MyHome(),
drawer: new MyDrawer(),
));
}
This works as expected on app start so on the surface it seems that I have implemented the InheritedWidget pattern correctly in my AuthenticatedWidget, but when I return back to the home page (MyHome) from elsewhere like this:
Navigator.popAndPushNamed(context, '/home');
This call-in the build method of MyHome (which worked previously) then results in authWidget being null:
final authWidget = AuthenticatedWidget.of(context);
Entirely possible I'm missing some nuances of how to properly implement an IW but again, it does work initially and I also see others raising the same question (i.e. here under the 'Inherited Widgets' heading).
Is it therefore not possible to use a Scaffold or a MaterialApp as the child of an InheritedWidget? Or is this maybe a bug to be raised? Thanks in advance!
MyInherited.of(context) will basically look into the parent of the current context to see if there's a MyInherited instantiated.
The problem is : Your inherited widget is instantiated within the current context.
=> No MyInherited as parent
=> crash
The trick is to use a different context.
There are many solutions there. You could instantiate MyInherited in another widget, so that the context of your build method will have a MyInherited as parent.
Or you could potentially use a Builder to introduce a fake widget that will pass you it's context.
Example of builder :
return new MyInheritedWidget(
child: new Builder(
builder: (context) => new Scaffold(),
),
);
Another problem, for the same reasons, is that if you insert an inheritedWidget inside a route, it will not be available outside of this route.
The solution is simple here !
Put your MyInheritedWidget above MaterialApp.
above material :
new MyInherited(
child: new MaterialApp(
// ...
),
)
Is it therefore not possible to use a Scaffold or a MaterialApp as the
child of an InheritedWidget?
It is very possible to do this. I was struggling with this earlier and posted some details and sample code here.
You might want to make your App-level InheritedWidget the parent of the MaterialApp rather than the Scaffold widget.
I think this has more to do with how you are setting up your MaterialWidget, but I can't quite tell from the code snippets you have provided.
If you can add some more context, I will see if I can provide more.