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
Related
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
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.
I work on flutter project . when i click to modify icon to edit name for example ==> the screen is roaleded automatically . How i can stop refresh screen after click on edit button ?
this piece of my Form code :
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Adresse email :',
style: TextStyle(
color: Color(0xFF4053FCF),
fontSize: 16,
fontWeight: FontWeight.w600
),
),
IconButton(
icon: Icon(CommunityMaterialIcons.pencil,
color: Colors.grey,
),
onPressed: () {
emailNode.requestFocus();
setState(() {
enableemail = true;
});
})
],
),
void editUserProfile() async {
setState(() {});
// if (_formKey.currentState.validate()) {
String name = _nameController.text;
String email = _emailController.text;
String adress = _adressController.text;
userApi.editUserProfile(name, email, adress).then((data) {
print(data);
if (data != null) {
// Navigator.pop(context);
/* Navigator.push(
context, MaterialPageRoute(builder: (context) => Profile()));*/
}
// setState(() {});
/* Navigator.push(
context, MaterialPageRoute(builder: (context) => BoxSettings()));*/
setState(() {
enableup = false;
enableadress = false;
enableemail = false;
});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(data)));
// ScaffoldMessenger.of(context).showSnackBar(snackBar3);
}).catchError((error) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(error.toString())));
});
setState(() {});
}
and this my screen for more information :
How i can press on edit button without reload screen ?
There some workarounds to achieve this (i.e. update the state of one widget after tapping a completely different widget) like passing the callback function as a parameter etc.
But The best and neat solution here which will solve the above problem and keep your code neat is using Provider pattern.
If you are not aware of how a Provider pattern works, you can easily google search for articles regarding it. Here is one of them :
https://www.raywenderlich.com/6373413-state-management-with-provider
Read the above article before moving below.
Basically what we do is :
Create a ChangeNotifier class.
Wrap the parent of both widgets by a ChangeNotifierProvider widget.
Wrap the widget you want to update with Consumer widget.
Then in your onTap/onPressed function of Edit button you can call a function which will call the notifyListener() function. What this will do is it will notify the above ChangeNotifierProvider widget that some change has neen occured in it's widget tree. Then it will traverse the child whole widget tree below and will update the widget wrapped with Consumer widget.
So this way, you wont need to refresh your whole screen and you can easily update one widget by doing some action on a competely different widget.
Wrap the widgets you want to refresh inside stateful builder and make the whole screen a stateless widget and then call stateful builder
I have a form on my page.
When I fill out the form, if it is valid a loading pop up appears and I start to perform calculations. Depending on the state of the calculations, the message on the loading popup will change.
I'm trying to do a function in my provider which will change the message depending on the state of the popup.
When I print the values they are displayed correctly but my message does not change in the widget
How I call my screen :
return MultiProvider(
providers: [
ChangeNotifierProvider<FormCalculatorNotifier>(
create: (BuildContext context) => FormCalculatorNotifier()
),
ChangeNotifierProvider<LoaderNotifier>(
create: (BuildContext context) => LoaderNotifier()
),
],
child: CalculatorScreen(),
);
In my screen, how I call the popup and calculations:
Expanded(
child: ButtonComponent.primary(
context: context,
text: AppTextButton.CALCUL,
onPressed: () async {
await _formProvider.submitForm(context);
if(_formProvider.state == FormProviderState.isSuccess){
// --------------------------------------
// Here I call the popup with the message variable
DialogComponent.loadingPopUp(context: context, description: _loaderProvider.message);
// --------------------------------------
// Here I try to change the popup message because I have my calculations
_loaderProvider.state = LoaderState.isLoadingJsons;
await _loaderProvider.updateMessage();
}
},
),
),
The popUp notifier:
class LoaderNotifier with ChangeNotifier {
LoaderState state = LoaderState.isReady;
String message = "Start...";
Future<void> updateMessage() async{
print('update message');
if(state == LoaderState.isLoadingJsons){
message = "Loading files...";
}
notifyListeners();
}
}
EDIT: I just saw that my problem is that I call the updateMessage function after calling it from the DialogComponent
If I want to follow my logic which is to study if the form is good to display the popup then perform the calculations and according to the calculations change the message of the popup, what to do since I will call the updateMessage function afterwards?
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();
},
)
],
),
),
);
}
}