Flutter Create a method including setState - flutter

I am trying to create a custom snackbar. It includes a "setState" executed after the snackbar dismisses. But the setStat function doesn't work (i.e., snackBarIsOn = false;). The snackbar pops up and dismisses after 2 seconds. It is also supposed to change "snackBarIsOn" to "true" but it doesn't happen. I tested it in the main code without the method. It works as it is supposed to. I am suspicious about the type of "actionAfterClosed".
Please help me.
customSnackBar(
context: context,
content: 'Enter a Title.',
label: 'Close',
textColor: Colors.white,
snackBarAction: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
setState(() {
snackBarIsOn = false;
});
},
actionAfterClosed:
setState(() {
snackBarIsOn = false;
})
);
void customSnackBar(
{required BuildContext context,
required String content,
required String label,
required Color textColor,
required VoidCallback snackBarAction,
required **VoidCallback** actionAfterClosed}) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(
content: Text(content),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: label,
textColor: textColor,
onPressed: snackBarAction,
),
))
.closed
.then((_) =>
actionAfterClosed
);
}

I believe this is the problem:
actionAfterClosed:
setState(() {
snackBarIsOn = false;
})
Here, you are telling flutter to run setState, and then use whatever it returns as a callback, so it expects setState to return a function to call after the snackbar closes, instead of calling setState itself.
To fix this, you must use a function literal like you did for snackbarAction:
actionAfterClosed:
() => setState(() {
snackBarIsOn = false;
})
The second problem I can find is here:
.closed
.then((_) =>
actionAfterClosed
);
You forgot the parenthesis! this way the function isn't running!
.closed
.then((_) =>
actionAfterClosed()
);

Related

How to Navigator.push() from a DropdownMenuItem's onTap and await the returned value?

Following on from Unhandled Exception: type '_DropdownRouteResult<int>' is not a subtype of type 'int?' when returning value from Navigator.pop() as I still haven't resolved the issue.
I have a DropdownFormField which I am dynamically populating from a db via a Provider. I would like to add a DropdownMenuItem which, when selected, pushes a new route (for inserting a new record into the db).
The route returns the id of the newly-inserted db record when popped, and I would like to set the new value as the value of the DropdownFormField.
Implementing the new item with a TextButton child and pushing in the buttons' onPressed results in expected push/pop behaviour, but is styled inconsistently from the "normal" items, and does not close the dropdown (which makes sense as the button is pressed, but the DropdownMenuItem is not tapped). Tapping outside the dropdown after popping reveals that the dropdown's value is updated correctly.
DropdownMenuItem<int>(child: TextButton(
onPressed: () async {
final int newValue = await Navigator.push(context, AddNewTeaProducerRoute());
setState(() {
_selectedValue = newValue;
});
},
child: Text('Add New Manufacturer')));
Implementing the new item with a Text child and pushing in the DropdownMenuItem's onTap (which seems like the correct approach) results in an immediate attempt to return the value, disrespecting the asynchronous nature of the onTap and resulting in the exception from my previous question. Breakpoint debugging without specifying the type of newValue shows that it is immediately assigned the Future/_DropdownRouteResult<int>, rather than awaiting its returned int.
DropdownMenuItem<int>(
onTap: () async {
final int newValue = await Navigator.push(context, AddNewTeaProducerRoute());
setState(() {
_selectedValue = newValue;
});
},
child: const Text('Add New Manufacturer'));
I have no idea why await is being respected in TextButton.onPressed but not in DropdownMenuItem.onTap
I don't know if it's the right way, since it relies on null as a placeholder value and I can't see how you'd easily scale it beyond a single DropdownMenuItem with special behaviour (as unlikely as it seems that you'd want to) but after reading this for the third time I finally grokked a solution - return null as the value, and perform navigation/assignment in the DropdownButtonFormField's onChanged
final brokenAddNewTeaProducerButton = DropdownMenuItem<int>(
value: null,
child: const Text('Add New Manufacturer'));
return DropdownButtonFormField<int?>(
value: _selectedValue,
items: [brokenAddNewTeaProducerButton] + teaProducerListItems,
onChanged: (value) async {
if (value == null) {
final newTeaProducerId = await Navigator.push(context, AddNewTeaProducerRoute());
setState(() {
_selectedValue = newTeaProducerId;
});
} else {
setState(() {
_selectedValue = value;
});
}
},
hint: Text('Select a manufacturer'),
);
}
**You can try this statfulBuilder**
StatefulBuilder(builder: (context,state){
return DropdownMenuItem<int>(child: TextButton(
onPressed: () async {
var newValue = await Navigator.push(context,
AddNewTeaProducerRoute());
state(() {
_selectedValue = newValue;
});
},
child: Text('Add New Manufacturer')));
}),

How to create undo function in SnackBar widget that restore Firestore data

I want my Flutter apps to be able to restore deleted data in my ListTile widget as well as in my Firestore. I have SnackBar as below:
return Dismissible(
key: Key(documentID),
onDismissed: (direction) {
var deleteItem = list.removeAt(index);
setState(() async{
isDeleting = true;
if (direction == DismissDirection.endToStart) {
await deleteData(log_id: documentID); // To delete the data in firebase
deleteItem;
ScaffoldMessenger.of(context).removeCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 3),
content: Text("${ds['food_name']} has been deleted",
style: TextStyle(color: Colors.white)),
action: SnackBarAction(
label: "UNDO",
textColor: AppColors.MAIN_COLOR,
onPressed: () {
list.insert(index, deleteItem); // this didn't work in my case
},
),
),
);
}
});
setState(() {
isDeleting = false;
});
},
I managed to delete the data in the ListTile and Firestore but im unable to restore them in both ListTile and Firestore with this line list.insert(index, deleteItem) once 'UNDO' is pressed.
This is how I declared the list -> final list = snapshot.data.docs; So, how do I achieve this?
I dont need an AlertDialog (I've seen this example when I was googling), I just want a SnackBar with 'UNDO' button that is able to perform the desired function. I am very new to Flutter/Dart so a little guidance is very much appreciated. Thank you in advance.
Updated code with the following solution
remove from list.
show Undo snackbar.
if no action then only delete from firebase.
if yes undo action then just add back to the list.
bool isUndoPressed = false;
#override
Widget build(BuildContext context) {
return Dismissible(
child: Container(),
key: Key(''),
onDismissed: (direction) {
if (direction == DismissDirection.endToStart) {
var deleteItem = list.removeAt(index);
setState(() {
isDeleting = true;
});
ScaffoldMessenger.of(context).removeCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 3),
content: Text("${['food_name']} has been deleted",
style: TextStyle(color: Colors.white)),
action: SnackBarAction(
label: "UNDO",
textColor: AppColors.MAIN_COLOR,
onPressed: () {
isUndoPressed = true;
setState(() {
isDeleting = false;
list.insert(
index, deleteItem); // this didn't work in my case
});
},
),
),
);
//start a 3s timer
Timer.periodic(const Duration(seconds: 3), (timer) async {
if (isUndoPressed) {
//can if undo button is pressed.
timer.cancel();
setState(() {
isDeleting = false;
});
} else {
await deleteData(
log_id: documentID); // To delete the data in firebase
isUndoPressed = false;
setState(() {
isDeleting = false;
});
}
});
}
});
}
The list.insert(index, deleteItem)in setState method is only able to restore data in ListTile but not in Firebase because once you delete a document in Firestore, it's gone for good. There is no undelete feature.
What you're trying to do is likely not very easy with the adapter provided by FirebaseUI. The only ways you can reasonably implement an undo feature is :
Have the delete function not actually delete the document, and
instead just update the UI to remove the view. You would have to
schedule the deletion to happen some time later. The undo option
would then just restore the visibility of the deleted document and
cancel the delayed deletion.
For that also add a new boolean field to the document to mark that
it's deleted or not, and use that in a filter for the query you pass
to FirebaseUI. The field would have to be present on every document
in order for it to filter correctly. After the UI is done, you would
then have to figure out how to actually delete all the documents you
marked for deletion in the UI.
Or you can create the document in another collection before deleting
it from the main one. For example, a common implementation is to have
a subcollection with deleted documents, or even the entire history of
each document.

Why using <Null> after showDialog?

Here I'm rendering an AlertDialog inside showDialog to display when error thrown from provider file. But that didn't work first time( Seems like stuck in catchError block, didn't execute future of catchError), Then I told to add <Null> after showDialog. That worked But How, What is changed, what it means?
Here is the code
if (_editedProduct.id == null) {
Provider.of<Products>(context, listen: false)
.addProduct(_editedProduct)
.catchError((error) {
return showDialog<Null>( //Here is that Null I didn't understand
context: context,
builder: (ctx) {
return AlertDialog(
title: Text('ERROR'),
content: Text('Error Occured'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('I GOT IT'),
)
],
);
});
}).then((_) {
print('THEN');
setState(() {
isLoading = false;
});
Navigator.of(context).pop();
});
} else {
Provider.of<Products>(context, listen: false)
.updateProduct(_editedProduct.id, _editedProduct);
Navigator.of(context).pop();
}
}
Nb: isLoading true shows a circluarProgressIndicator()
From the Official Docs
Returns a Future that resolves to the value (if any) that was passed to Navigator.pop when the dialog was closed.
The place where you sent Null indicates the type of data the Future will return.
By using Null you are indicating that the Future won't return any data, this tell your program not to wait for the possible value that might be returned.
Suppose in your dialog user has to pick 2 numbers and you want the picked number to be returned to the place where you called the showDialog() function then you'll use int instead of Null.
Something like
showDialog<int>(...)

I need to press the button 2 times in order to function (flutter)

I have a problem implementing a login button, when I press it, it will not function at first, but when I press it again (same value fields) it works.
here's my code
Button:
Center(child: RaisedButton(onPressed: (){
setState(() {
onPressedLogin(userName.text,password.text);
});
}
The OnPressedLogin():
void onPressedLogin(String userName,String password) async{
bool isValid = false;
var value = await dataBaseHelper.getUserList();
for(User userOB in value){
//print(userOB.password+" "+password),
if(userName == userOB.username && password == userOB.password) {
isValid = true;
this.password.clear();
this.userName.clear();
inputTextColor = Colors.grey[850];
invalidCredentials = "";
print("YES");
//Navigator.push(context, MaterialPageRoute(builder: (context) => Home()));
break;
}
}
if(!isValid){
inputTextColor = Colors.red[800];
invalidCredentials = "Invalid Credentials";
}
You are using a Future but in setState() you are not waiting for it so that's way it work in the second press (the value take time to change).
To make it work with single press you have to a wait the Future to complete before rebuilding, here how:
First change the return type of the function
Future<void> onPressedLogin(String userName,String password)
Then in the RaisedButton
onPressed: () async {
await onPressedLogin(userName.text,password.text);
setState(() {});
},
The moment you setState(), the UI will refresh!
Probably that's the issue, let me explain:
What you should do is to call your function before setState(), so that the screen is refreshed with the new info.
Center(child: RaisedButton(onPressed: (){
onPressedLogin(userName.text,password.text);
setState(() {
//Variables that change for the refresh.
});
}
In your specific case, I don't see the need for SetState() as you are only printing values in log, not changing the UI.
Hope it is helpful.

Need to execute task after setState finish

I need to execute a task, after setState() method complete its whole job. I will describe my problem at below with some code.
I have a login screen and it has a widgets as below:
...
child: TextField(
errorText: getErrorStringOfEmail(),
...
),
...
child: MaterialButton(
onPressed: () => onClickLoginButton(),
...
),
"getErrorStringOfEmail" method is as below: (This method is called when the Textfield is updated by calling "setState()".)
String getErrorStringOfEmail(
if(loginEmailTextEditingController.text.toString() == "a") {
isValidLogin = false;
return 'Wrong password or email';
}
isValidLogin = true;
return null;
}
"onClickLoginButton" is as below:
void onClickLoginButton() {
setState(() {
//in here a boolean is changed to update Textfield.
});
if (isValidLogin) {
Navigator.pushReplacement (
context,
MaterialPageRoute(builder: (context) => HomeWidget()),
);
}
}
As you can see, "isValidLogin" boolean is assigned when Textfield widget is updated. However, getErrorStringOfEmail method is called after onClickLoginButton. I need to execute the following part,
if (isValidLogin) {
Navigator.pushReplacement (
context,
MaterialPageRoute(builder: (context) => HomeWidget()),
);
}
after getErrorStringOfEmail method is called. To achieve this, i need to call this part after setState update widgets.
How can i do this?