Flutter - Confirm closing of Dialog on Back button? - flutter

I'm currently trying to figure out how to prevent a dialog from closing directly by the back button.
For Dialogs, I have a base class, which I give a Widget with content.
Now I have a Screen, where the user gets to enter something in a dialog, and when the user presses the cancel button, a second Dialog pops up on top of the existing dialog.
With this second dialog, which also uses the base class, I can confirm or abort closing the previous dialog.
But when I use the back button of my phone, the dialog instantly closes, without asking to confirm.
Already tried wrapping the Dialog in the base class with WillPopScope, but that only resulted in the Dialog not closing anymore (maybe I did something wrong there, but I don't have the code of that try anymore)
Here is the code of the base class (I shortened it a bit by removing styling etc.):
class DialogBase {
final bool isDismissable;
final Function()? doAfterwards;
final Function(bool)? onConfirmClose;
DialogBase(
{required this.isDismissable, this.doAfterwards, this.onConfirmClose});
show(BuildContext context, Widget content) async {
await showDialog(
barrierDismissible: isDismissable,
context: context,
builder: (context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(20),
child: content,
),
);
}).then((value) {
if (onConfirmClose != null && value != null) {
onConfirmClose!(value);
}
}).whenComplete(() async {
if (doAfterwards != null) {
doAfterwards!();
}
});
}
}
In a Dialog Content widget, the abort button does the following onPressed:
onPressed: () async {
await DialogBase(
isDismissable: false,
onConfirmClose: (bool result) {
if (result) {
Navigator.of(context).pop();
}
}).show(context, const ConfirmAbortDialogContent());
},
And the dialog itself gets called this way (which is also called on a button pressed:
void openAddNewPostDialog(BuildContext context) {
DialogBase(
isDismissable: false).show(context, const AddThreadPostDialogContent());
}
Any way on how to prevent a dialog from closing when the back button is used (and instead ask user to confirm)?

Try WillPopScope
Using onWillPop value I think you can achieve what you want

Related

how to disable user to go back after successful login in flutter

I have this login function and it's working like charm , but the problem that the user can press the back button and return to the login screen and I want to disable that.
void _login() async {
setState(() {
_isLoading = true;
});
var data = {'email': email, 'password': password};
print("Data =" + data.toString());
var res = await Network().authData(data, '/login');
var body = json.decode(res.body);
print(body);
if (body['success']) {
SharedPreferences localStorage = await SharedPreferences.getInstance();
localStorage.setString('access_token', json.encode(body['access_token']));
localStorage.setString('user', json.encode(body['user']));
if (body['user']['verified'] == 1) {
// OPEN THE HOME PAGE AND BLOCK THE BACK ACTION
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HomeScreen()),
);
} else {
showAnimatedDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
content: Container(
width: 100,
height: 50,
child:
Center(child: Text("Your account isn't verified yet!")),
));
},
animationType: DialogTransitionType.fadeScale,
curve: Curves.fastOutSlowIn,
duration: Duration(seconds: 1),
);
}
} else {
_showMsg(body['message']);
}
setState(() {
_isLoading = false;
});
}
Plus , is there a way also to keep him logged even after closing the app once the login is done ? (Until the user press a logout button).
Firstly: Let us understand how Screens are organized in Flutter Framework:
The screens in Flutter are organized in a Stack fashion, so if you are on Screen A and you pushed the Screen B, then from Screen B you pushed Screen C, your stack would look like the following:
After Pushing Screen B:
Screen A
Screen B (Current Screen)
After Pushing Screen C:
Screen A
Screen B
Screen C (Current Screen)
So let's say Screen B is your Login Screen, and you want to block the user from going back, Then you don't have to just Push the screen into the stack, instead, you should Replace the screen in the stack with your Home Screen.
Secondly: Now after understanding the flow, let us talk about the code:
In your scenario, instead of using Navigator.push() you have to use Navigator.pushReplacement() to replace the screen in the stack as mentioned above.
Bounce:
If you are in a scenario that needs all the screens to be removed from the stack and keep only the screen that would be pushed, as an example, if you have a multi-step registration flow, and after successful registration, you want to pop all the screens of the process from the stack and pushing the home screen, then you may use the following trick:
Navigator.popUntil((route) => route.isFirst); // Execute it before pushing the screen in order to keep only one route (screen) in the stack.
Navigator.pushReplacement(); // Now you are replacing the only screen in the stack with the new one so the stack will only contain your wanted screen.
I kind of find a way to do it , I don't know if it's the good way to do it but this did the job .
Wrapping the scaffold with WillPopScope like this :
WillPopScope(
onWillPop: () async => false,
)
I got it from here
Here first you need to check whether the user is authenticated....
You have to check this condition's into material app's home : parameter.
if (snapShot.connectionState == ConnectionState.waiting) {
return const SplashScreen(); // If it's in waiting state it will show splash screen
}
if (snapShot.hasData) {
return const homeScreen(); // If user is authenticated it will show home screen
}
return const LoginScreen(); // If user is not authenticated then it will show login screen
here, snapShot is all about user data.
Thanks in advance.
To block the user from moving back to the previous screen, try Navigator.pushReplacement instead of Navigator.push.

navigation using bloc 8 and auto_route 3.2.0

I'm trying to show screens using auto_route, based on state kept using bloc. I have two objectives:
navigate when button is clicked (e.g. login)
if at any point, the state changes, again navigate to the correct screen (e.g. session expiry)
for usecase 1 I have a button:
child: ElevatedButton(
onPressed: () {
final sessionBloc = BlocProvider.of<SessionBloc>(context);
sessionBloc.add(SignInEvent());
},
child: const Text('Login'),
),
this delegates to my session bloc that has a handler that is defined as:
emit(
SignedIn(
const User(
displayName: 'displayName',
phoneNumber: 'phoneNumber',
idNumber: 'idNumber',
photoUrl: 'photoUrl'),
),
);
and I'm trying to use an authguard defined as:
class AuthGuard extends AutoRouteGuard {
AuthGuard({required Stream<SessionState> sessionBloc}) {
sessionBloc.listen((state) {
print('['+state.toString()+']');
_authenticated = state is SignedIn;
});
}
bool _authenticated = true;
#override
void onNavigation(NavigationResolver resolver, StackRouter router) {
if (!_authenticated) {
router.push(
LoginRouter(),
);
}
resolver.next(true);
}
}
I can see the print statement everytime I press the button, so that works fine (no surprise).
The problem is that I don't get any navigation to another screeen when pressing the button. I can only assume this is because although I'm updating the state and the authguard is aware of the new state, it does nothing till we actually try navigating i.e. onNavigate is called.
So how do I do that, how/where do I trigger navigation?
I would use either BlocConsumer or BlocListner and inside the listen method do the navigation to the desired screen. And make sure there is a route guard on the route you want to navigate to after authentication.
BlocListener<SessionBloc, SessionState>(
listener: (context, state) {
if(state is SignedIn){
//TODO: route to the page you want to go to
}
},
child: ElevatedButton(
onPressed: () {
final sessionBloc = BlocProvider.of<SessionBloc>(context);
sessionBloc.add(SignInEvent());
},
child: const Text('Login'),
),
);

Closing ModalBottomSeet before time on Flutter causes black screen

I'm trying to create a modal bottom sheet using showModalBottomSheet, which wil display a form to register a todo item. The idea is that once the todo item is registered, I want to display a check icon from some seconds and then automatically close the sheet.
here is the snippet:
FloatingActionButton _floatingActionButton(BuildContext context) {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
await _showBottomSheet(
context: context,
content: CreateTodoForm(
onClose: () {
...
Navigator.pop(context);
},
),
);
},
);
}
and inside the CreateTodoForm widget:
class _CreateTodoFormState extends State<CreateTodoForm> {
TextEditingController titleController = TextEditingController();
bool completed = false;
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<TodoFormBloc>(),
child: BlocBuilder<TodoFormBloc, ITodoFormState>(
builder: (context, state) {
...
if (state is SubmittedTodo) {
Future.delayed(Duration(seconds: 2), widget.onClose);
return Container(
height: 127,
child: Icon(Icons.check, size: 50, color: Colors.white),
);
}
...
},
),
);
}
Has you can see, when the state is SubmittedTodo (todo was submitted successfully) I return a container with the check icon, and after 2 seconds I call the onClose Function which is a call to Navigator.pop(context) to close the sheet.
This works great but it has a problem... if the user taps the < button on the device, or swipe the sheet down to dismiss it, before the 2 seconds are completed, then the sheet closes due to the user action, and then the future completes and it basically closes the app (the app get full black screen).
So my quiestion is how can I close the sheet automatically after some time safely without worring about what the user does.
Probably this is happening because of Navigator.pop(context); getting called after you click the back button which cause two pop. and the black screen is shown because there is no other screen to navigate back to.
As a solution i propose wrapping your form widget by WillPopScope and then you will get notified that the user clicked on the back button. here you can close your form by calling onClose

How do I interact with UI and async functions by a button press?

So I have a login button that would initiate an async function logMeIn() for logging in, which returns a Future<bool> indicating if logging in was successful.
What I want to achieve is after pressing the button, the text within the button would change into a CircularProgressIndicator, and become plain text when logMeIn() has finished.
I know that I could use a FutureBuilder for one-time async tasks, but I simply can't think of a way to use it here.
How am I supposed to achieve this?
Thanks in advance.
Edit
I didn't provide more information because I'm pretty sure it wasn't how it's supposed to be done. But I'd share what I tried whatsoever.
My original button looks somewhat like this:
RaisedButton(
child: Text("Login")
)
I have a async login function that returns a Future<bool> which indicates if the login is successful or not.
Future<bool> logMeIn() async {
// login tasks here
}
What I want to do is to change the button to the following and run logMeIn() when I press the button, and change it back to the plain text version after it's finished:
RaisedButton(
child: CircularProgressIndicator()
onPressed: () {}
)
What I tried
I tried adding a logging_in boolean as a flag to control it, here I call the button with plain text StaticButton(), the other named ActiveButton:
Then I wrapped the ActiveButton with a FutureBuilder:
logging_in ? FutureBuilder(
future: logMeIn(),
builder: (_, snapshot) {
if(snapshot.hasData)
setState(() {
logging_in = false;
});
else
return ActiveButton();
}
) : StaticButton();
and when I pressed the button it would setState and set logging_in = true.
StaticButton:
RaisedButton(
child: Text("Login"),
onPressed: () {
setState(() {
logging_in = true;
});
}
)
The above is what I tried. Should accomplish the effect, but is definitely not elegant. Is there a better way to achieve this?
What you have isn't that far off. You have a logging_in variable that tells you when to show the CircularProgressIndicator already, which is a good start. The only thing is that FutureBuilder has no use here. It's meant for retrieving data from Futures that need to be displayed in the UI.
What you need to do is call logMeIn in the onPressed of your "Static button" as shown here:
StaticButton:
RaisedButton(
child: Text("Login"),
onPressed: () async {
setState(() {
logging_in = true;
});
await logMeIn();
setState(() {
logging_in = false;
});
}
)
You don't need to switch out the whole button when logging_in changes, just the child of the button.
StaticButton(with child switching):
RaisedButton(
child: logging_in ? CircularProgressIndicator() : Text("Login"),
onPressed: () async {
setState(() {
logging_in = true;
});
await logMeIn();
setState(() {
logging_in = false;
});
}
)
You also probably want to add another bool to prevent multiple clicks of the button while its loading. As a note in using FutureBuilder and essentially any other builder in flutter, you always need to return a widget from the builder function, which your current implementation does not do.

Refresh the back page when the device back button is clicked in flutter

I have a page called appointments When i click on appointments(i pass the reports to the next page) the reports are displayed on the reports page,When i add a new report to the page and when i clicked the device back button and again go the reports page the updated files are not seen .I again have to load the entire page then i'm able to view the reports.
I'm loading the appointments in the initState() of the page into FutureBuilder.
Can Anyone suggest me how to refresh the data when i click my back button from the device
You can do like this:
In your report page, when button is clicked, open Add Report Page. When the back button is pressed, it will call the _refreshData method.
Navigator.pushNamed(context, AddReportPage.ROUTE).then((onValue) {
_refreshData()
});
The _refreshData is used to load the entire page, which you use in initState.
To open new page you should use next
void _openButtonHandler() {
Navigator.of(context).pushNamed("route123").then((result) {
if (result != null) {
refreshCall();
}
}).catchError((error) {});
}
The back button method should be like this:
Navigator.pop(context, {"param1": "value1"});
You need to use RefreshIndicator for this scenario.Your refresh indicator widget will be the parent of AppointmentsPage.We will assign a key so that we can call it from anywhere and when reports page is closed we will refresh page using this key.I have created a small example along with comments to demonstrate you how it will be done.
class AppointmentsPage extends StatefulWidget {
#override
_AppointmentsPageState createState() => _AppointmentsPageState();
}
class _AppointmentsPageState extends State<AppointmentsPage> {
Future appointments;
var refreshKey = GlobalKey<RefreshIndicatorState>();
#override
void initState() {
super.initState();
//Your method to fetch appointments.
getAppointments();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
key: refreshKey,
color: Colors.black,
onRefresh: () async {
//Call appointments future and fetch appointments.When they are fetched
//call setState to refresh page
getAppointments();
await appointments;
setState(() {});
},
child: Column(
children: <Widget>[
FutureBuilder(),
RaisedButton(
child: Text("Go To Report"),
onPressed: () async {
//Use await and then navigate to report page
await Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ReportPage(report: report));
);
//After popped back from report page call refresh indicator to refresh page
refreshKey.currentState.show();
},
)
],
),
),
);
}
}