How to call nested conditional void function with onPressed [flutter] - flutter

I have a customized flutter stepper widget and I want to having conditional back button when I press the backbutton in appbar, it will be back on previous step, just like this
onPressed: () {
if (currentStep != 0) {
onStepCancel;
} else {
Navigator.pop(context);
}
},
and somehow onStepCancel is can't be call because it has a value of final VoidCallback? onStepCancel, I put another function inside another function A.K.A nested, I want to use this widget in another class, so it can be simplify by only putting the void function inside onStepCancel
CustomStepper(
. . .
currentStep: controller.currentStep.value,
onStepContinue: controller.increment,
onStepCancel: controller.decrement,
);
the void function that fill with decrement function of currentStep will be proceed inside onStepCancel and when user click on back button with condition currentStep != 0 it will show the previous step and when user is reaching currentStep == 0 it will back to previous page Navigator.pop(context);, but the problem is onStepCancel can't be VoidCallback? because the conditional function is already the void function itself and it can't be returned VoidCallBack? inside it, so how can I call onStepCancel function inside conditional case with onPressed, this is the widget function where I put it:
Widget _buttonBack(int stepIndex, BuildContext context) {
return IconButton(
onPressed: () {
if (currentStep != 0) {
onStepCancel;
} else {
Navigator.pop(context);
}
},
icon: SvgPicture.asset(
Images.backArrowButton,
color: ColorResources.brandHeavy,
),
);
}
and I want to put Widget _buttonBack inside:
Scaffold(
appBar: AppBar(
. . .
leading: _buttonBack(currentStep, context),
. . .
);
edit:
onStepCancel is fullfil with void function that I call from controller:
void decrement() => currentStep--;
void decrement() is the function for getting back to previous Step and I call decrement() function in onStepCancel just like this:
onStepCancel: controller.decrement
Inside my CustomStepper class, onStepCancel will pass decrement to onPressed(), I have try few way to put conditional onPressed(), with this:
onPressed: () => currentStep != 0 ? onStepCancel : Navigator.pop(context),
and this
onPressed: () {
if (currentStep != 0) {
onStepCancel;
} else {
Navigator.pop(context);
}
},
both conditional doesn't work, but the funny thing is when I call onStepCancel without conditional case, like this:
onPressed: onStepCancel
it show no problem and it works well, so the point is, on step cancel will only work well without nested function, I only could call it with onStepCancel without any conditional function, how to call nested function? is it needed to be in any other form instead of function or else?

if onStepCancel is a callback, you should call it instead of returning it:
if (currentStep != 0) {
onStepCancel();
} else {
Navigator.pop(context);
}

Related

Flutter: how to execute multiple functions in a single button

in Flutter I have made a custom button that does a small animation when pressing it. The issue is that when I add a VoidCallBack function, which is a parameter that I give to the button widget, to the same onTap, then the function does not get executed whilst the animation does.
I did find this question ( how do we execute two function in single button pressed in flutter) that seems to be similar to mine but I have tried the suggestions and they do not work for me.
Here is a code snippet of when Im trying to use the button widget:
MyButton (
onTap = () {
print(_isSelected);
setState(() {
_isSelected[0] = !_isSelected[0];
});
},
)
Also I dont know if it makes a difference but Ive tried it both with and without the setState part.
Then in the button itself I do:
onTap: () {
widget.onTap;
setState(() {
_clicked = !_clicked;
});
},
I also tried to do this, like in the other stackOverflow question which had the same result:
onTap: () => [
widget.onTap,
{
setState(() {
_clicked = !_clicked;
})
}
],
Though it does work if I only use the parameter: onTap: widget.onTap,
This is usually how I do it.
onTap: () {
widget.onTap();
your_function_here();
},
Declare function variable:
final Function onTap;
and call widget.onTap with round brackets as below:
onTap: (){
widget.onTap();
setState(() {
_clicked = !_clicked;
});
}
You need to declare a constructor which takes onTap function as a parameter.
class MyButton extends StatelessWidget {
final Function onTap;
const MyButton ({Key? key, this.onTap,}) : super(key: key);
}
Since the onTap parameter needs a function as argument, you simply write a function that runs your functions.
MyButton(
onTap: () {
your_function1();
your_function2();
},
)

How do I update a widget UI on button press then execute a function in Flutter?

When a widget icon button is pressed, I want to change the button's icon, have setState rebuild the widget so the changed button icon is visible, then run a function:
bool _showPauseIcon = false;
void doSomething() {
print("doSomething()");
}
.
.
.
IconButton(
icon: _showPauseIcon ? Icon(Icons.pause) : Icon(Icons.play),
onPressed: () {
_showPauseIcon = true;
setState (() { });
doSomething();
},
)
doSomething() appears to be called before setState rebuilds the widget, so the modified icon only appears after doSomething() has been called - I need it to happen before doSomething() is called. I looking for the simplest possible solution.
SetState is only for the updating the widget on Current page.. So no need to call function inside the setstate.
IconButton(
icon: _showPauseIcon ? Icon(Icons.pause) : Icon(Icons.play),
onPressed: () {
setState (() {
_showPauseIcon = true;
});
doSomething();
},
)
Solved:
onPressed: () {
setState(() {
_showPauseIcon = true;
});
SchedulerBinding.instance.addPostFrameCallback((_) {
doSomething();
});
}

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.

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?

setState does not seem to work inside a builder function

How does setState actually work?
It seems to not do what I expect it to do when the Widget which should have been rebuilt is built in a builder function. The current issue I have is with a ListView.builder and buttons inside an AlertDialog.
One of the buttons here is an "AutoClean" which will automatically remove certain items from the list show in the dialog.
Note: The objective here is to show a confirmation with a list of "Jobs" which will be submitted. The jobs are marked to show which ones appear to be invalid. The user can go Back to update the parameters, or press "Auto Clean" to remove the ones that are invalid.
The button onTap looks like this:
GeneralButton(
color: Colors.yellow,
label: 'Clear Overdue',
onTap: () {
print('Nr of jobs BEFORE: ${jobQueue.length}');
for (int i = jobQueue.length - 1; i >= 0; i--) {
print('Checking item at $i');
Map task = jobQueue[i];
if (cuttoffTime.isAfter(task['dt'])) {
print('Removing item $i');
setState(() { // NOT WORKING
jobQueue = List<Map<String, dynamic>>.from(jobQueue)
..removeAt(i); // THIS WORKS
});
}
}
print('Nr of jobs AFTER: ${jobQueue.length}');
updateTaskListState(); // NOT WORKING
print('New Task-list state: $taskListState');
},
),
Where jobQueue is used as the source for building the ListView.
updateTaskListState looks like this:
void updateTaskListState() {
DateTime cuttoffTime = DateTime.now().add(Duration(minutes: 10));
if (jobQueue.length == 0) {
setState(() {
taskListState = TaskListState.empty;
});
return;
}
bool allDone = true;
bool foundOverdue = false;
for (Map task in jobQueue) {
if (task['result'] == null) allDone = false;
if (cuttoffTime.isAfter(task['dt'])) foundOverdue = true;
}
if (allDone) {
setState(() {
taskListState = TaskListState.done;
});
return;
}
if (foundOverdue) {
setState(() {
taskListState = TaskListState.needsCleaning;
});
return;
}
setState(() {
taskListState = TaskListState.ready;
});
}
TaskListState is simply an enum used to decide whether the job queue is ready to be submitted.
The "Submit" button should become active once the taskListState is set to TaskListState.ready. The AlertDialog button row uses the taskListState for that like this:
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
if (taskListState == TaskListState.ready)
ConfirmButton(
onTap: (isValid && isOnlineNow)
? () {
postAllInstructions().then((_) {
updateTaskListState();
// navigateBack();
});
: null),
From the console output I can see that that is happening but it isn't working. It would appear to be related to the same issue.
I don't seem to have this kind of problem when I have all the widgets built using a simple widget tree inside of build. But in this case I'm not able to update the display of the dialog to show the new list without the removed items.
This post is getting long but the ListView builder, inside the AleryDialog, looks like this:
Flexible(
child: ListView.builder(
itemBuilder: (BuildContext context, int itemIndex) {
DateTime itemTime = jobQueue[itemIndex]['dt'];
bool isPastCutoff = itemTime.isBefore(cuttoffTime);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text(
userDateFormat.format(itemTime),
style: TextStyle(
color:
isPastCutoff ? Colors.deepOrangeAccent : Colors.blue,
),
),
Icon(
isPastCutoff ? Icons.warning : Icons.cached,
color: isPastCutoff ? Colors.red : Colors.green,
)
],
);
},
itemCount: jobQueue.length,
),
),
But since the Row() with buttons also doesn't react to setState I doubt that the problem lies within the builder function itself.
FWIW all the code, except for a few items like "GeneralButton" which is just a boilerplate widget, resides in the State class for the Screen.
My gut-feeling is that this is related to the fact that jobQueue is not passed to any of the widgets. The builder function refers to jobQueue[itemIndex], where it accesses the jobQueue attribute directly.
I might try to extract the AlertDialog into an external Widget. Doing so will mean that it can only access jobQueue if it is passed to the Widget's constructor....
Since you are writing that this is happening while using a dialog, this might be the cause of your problem:
https://api.flutter.dev/flutter/material/showDialog.html
The setState call inside your dialog therefore won't trigger the desired UI rebuild of the dialog content. As stated in the API a short and easy way to achieve a rebuild in another context would be to use the StatefulBuilder widget:
showDialog(
context: context,
builder: (dialogContext) {
return StatefulBuilder(
builder: (stateContext, setInnerState) {
// return your dialog widget - Rows in ListView in Container
...
// call it directly as part of onTap of a widget of yours or
// pass the setInnerState down to another widgets
setInnerState((){
...
})
}
);
EDIT
There are, as in almost every case in the programming world, various approaches to handle the setInnerState call to update the dialog UI. It highly depends on the general way of how you decided to manage data flow / management and logic separation. As an example I use your GeneralButton widget (assuming it is a StatefulWidget):
class GeneralButton extends StatefulWidget {
// all your parameters
...
// your custom onTap you provide as instantiated
final VoidCallback onTap;
GeneralButton({..., this.onTap});
#override
State<StatefulWidget> createState() => _GeneralButtonState();
}
class _GeneralButtonState extends State<GeneralButton> {
...
#override
Widget build(BuildContext context) {
// can be any widget acting as a button - Container, GestureRecognizer...
return MaterialButton(
...
onTap: {
// your button logic which has either been provided fully
// by the onTap parameter or has some fixed code which is
// being called every time
...
// finally calling the provided onTap function which has the
// setInnerState call!
widget.onTap();
},
);
}
If you have no fixed logic in your GeneralButton widget, you can write: onTap: widget.onTap
This would result in using your GeneralButton as follows:
...
GeneralButton(
...
onTap: {
// the desired actions like provided in your first post
...
// calling setInnerState to trigger the dialog UI rebuild
setInnerState((){});
},
)