Trigger a validate/submit form method from the AppBar in flutter - flutter

I have 3 widgets.
The HomeApp - A stateless Widget that connects everything.
A custom AppBarWidget - A Stateful Widget which have a submit button.
A SurveyForm Widget - A Stateful widget which have 50+ inputs including TextFormField, radio buttons, and a whole bunch of custom input types.
When the user press the submit button in the appbar, it needs to do some custom validations in the survey form, create a new model class. which translate into a JSON object, and get POST to an API.
I've read a few options,
Initiate all the variables and the validation/submit logics in the HomeApp, pass it down to the SurveyForm, and use callback to trigger the function. But it involves me passing down 50+ parameters to the SurveyForm.
Using a GlobalKey. Not really sure how it works, When I try to do final key = new GlobalKey<SurveyForm>();, It said 'SurveyForm' doesn't extend 'State<StatefulWidget>'..
Pass the parent instance to the child widget, suggested by this link.
Calling a method of child widget, but this link said it's discouraged.
Write a custom controller.
What would be the preferable way in Flutter?

You can use a Form widget Tutorial.
Basically you wrap all of your TextFields and checkbox and other stuff with a Form widget and set it's key to a GlobalKey<FormState>(). now you can use another type of fields in it(for example instead of TextaField you use a TextFormField) now you can set a property called onSaved on them(We're gonna use it in a bit). then whenever you want to submit the forms data, you call a function that you've written in your state class(by adding it to an onPressed value of a button). It's something like this:
void _submitForm() {
final form = _formKey.currentState; // this is your GlobalKey<FormState> that you specified and set to the key of your Form Widget.
if (form.validate()) { // form.validate() will call all the validator functions that you've passed to your inputfields
form.save(); // This will call all the onSaved functions that you passed to your forms.
}
}
It gets your Form widget, calls all the validator functions of the inputs, if everything fits together, calls all the onSaved functions of the inputs.
You can create a dictionary and in onSaved of the inputfields set the inputs values in it.
Now you can use json.encode(your_data_dict) to turn it to a JSON string or use your object. then post the json.

So I managed to solved this.
My problem should actual be break down into 3 problems.
Validate textformfield, radio buttons, all custom inputs.
Create a new model class that can be passed translate to JSON.
Submit the form from a button within another widget (Appbar).
In order to solve the first one, I wrapped everything in SurveyForm into a Form widget. This part is simple, there are many tutorials online. In order to do validation on custom inputs, I wrapped the input in a FormField widget, and trigger the didChange method of FormFieldState. This allows me to use validator and onSaved method of FormField using methods callback. Below is my working code for a datepicker.
class Q1<T> extends StatefulWidget {
final FormFieldValidator<DateTime> validator;
final FormFieldSetter<DateTime> onSaved;
const Q1({
Key key,
#required this.onSaved,
#required this.validator,
}) : assert(validator != null),
assert(onSaved != null),
super(
key: key,
);
#override
_Q1State createState() => _Q1State();
}
class _Q1State extends State<Q1> {
DateTime _completionDate;
#override
Widget build(BuildContext context) {
return FormField(
validator: (val) {
return widget.validator(val);
},
onSaved: (val) {
widget.onSaved(val);
},
builder: (FormFieldState<dynamic> field) {
return Row(
children: [
Expanded(
flex: 1,
child: Text('1. Completion Date'),
),
Expanded(
flex: 2,
child: Row(
children: [
Container(
padding: EdgeInsets.all(
SizeConfig().getProportionateScreenWidth(
kDatePickerIconTextBoxPadding),
),
decoration: BoxDecoration(
border: Border.all(
color: kDatePickerIconTextBoxBorderColor,
),
borderRadius: BorderRadius.all(Radius.circular(
SizeConfig().getProportionateScreenWidth(
kDatePickerIconTextBoxRadius),
) // <--- border radius here
),
),
child: Text(_completionDate == null
? 'Pick a date'
: DateFormat(kDateFormat).format(_completionDate)),
),
IconButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
).then((date) {
setState(() {
field.didChange(date);
_completionDate = date;
});
});
},
icon: FaIcon(FontAwesomeIcons.calendarAlt)),
],
),
),
],
);
},
);
}
}
And I just called the widget with
Q1(
validator: (val) {
if (val == null) {
return 'Completion Date is missing';
}
return null;
},
onSaved: (value) {
setState(() {
widget.questionnaire.completion_date = DateFormat(kDateFormat).format(value);
});
},
),
In order to solve the 2nd one. I used json_serializable, where the object can also be passed around between HomeApp and SurveyForm widget, so I don't need to pass around 50+ variables.
And for the 3rd problem, I initiated final _formKey = GlobalKey<FormState>(); in the HomeApp, and passed it down to the SurveyForm widget, and use onPressed callback in the Appbar to trigger validation/submit in the SurveyForm.
Bloc will be used for submitting JSON and load animation, but that's beyond this.
I'm new to flutter (Been using this for a month), please let me know if anyone have a better solution.

Related

Controlling state of a widget from another widget

I'm using the example in this link:
https://medium.com/#jb.padamchopra/implementing-a-bottom-app-bar-with-floating-action-button-flutter-463560e1324
Widget from the link
The floating action button animation is controlled by a bool clickedCenterFAB
In my code the Widgets inside the animated container are defined in a class and I want, after the user writes a text and presses the button, the animated container to collapse, so logically I want to call setState and assign clickedCenterFAB = false.
I'm new to flutter and I tried calling setState inside the uploadPage widget and try to change clickedCenterFAB but it didn't work. Any idea how to do it guys? thanks for the help in advance.
My widget
AnimatedContainer(
duration: Duration(milliseconds: 300),
//if clickedCentreFAB == true, the first parameter is used. If it's false, the second.
height: clickedCentreFAB
? MediaQuery.of(context).size.height
: 10.0,
width: clickedCentreFAB
? MediaQuery.of(context).size.height
: 10.0,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(clickedCentreFAB ? 0.0 : 300.0),
color: Colors.blue,
),
child: clickedCentreFAB ? UploadPage() : null)
EDIT: After following with Michael's solution and keeping up with the comments, this is where I got
class UploadPage extends StatefulWidget {
final Function collapseContainer;
UploadPage(this.collapseContainer);
#override
_UploadPageState createState() =>
_UploadPageState(collapseContainer);
}
class _UploadPageState extends State<UploadPage> {
final Function collapseContainer;
_UploadPageState(this.collapseContainer);
...
...
CustomButton(
() {
if (_controller.text.trim().isNotEmpty) {
DatabaseServices().uploadPost(
FirebaseAuth.instance.currentUser.uid,
_controller.text,
!_isChecked);
collapseContainer;
}
},
'Post',
Colors.white,
),
CustomButton obviously is a widget class that i created to avoid redundency and in the params in a Function which I'm passing here
The reason you can't call setState directly from UploadPage is because setState is a class method, so it's always only associated with the state of the immediately enclosing class instance. If you want to change the state of a Widget that's higher up in the Widget tree, the standard method is to pass a callback to the child component.
In your case, you might add a new property to your UploadPage widget - something like, onButtonPressed:
class UploadPage extends StatelessWidget {
final Function onButtonPressed;
UploadPage({required this.onButtonPressed});
#override
Widget build(BuildContext context) {
return YourWidget(
// ...
child: RaisedButton(onPressed: onButtonPressed),
// ...
);
}
}
Then:
AnimatedContainer(
// ...
child: clickedCentreFAB
? UploadPage(
onButtonPressed: () {
setState(() { return clickedCentreFAB = false; });
},
)
: null,
)
)
As an aside, at a certain point, the state of your app may be complex enough to warrant solutions like bloc, redux, etc - However, for this particular scenario those might be overkill.
If you find yourself having to pass callbacks and properties through several levels of the widget tree, then provider can be immensely helpful.
If the mapping between actions, states, and views becomes very complex, then bloc or redux can help to impose structure onto your project and make it easier to reason about, though you will often need to write more code to get the same tasks done.

Flutter GetX forms validation

I am looking for an example of how to handle forms and validation in best practice with GetX?
Is there any good example of that or can someone show me an example of how we best can do this?
Here's an example of how you could use GetX's observables to dynamically update form fields & submit button.
I make no claim that this is a best practice. I'm sure there's better ways of accomplishing the same. But it's fun to play around with how GetX can be used to perform validation.
Form + Obx
Two widgets of interest that rebuild based on Observable value changes:
TextFormField
InputDecoration's errorText changes & will rebuild this widget
onChanged: fx.usernameChanged doesn't cause rebuilds. This calls a function in the controller usernameChanged(String val) when form field input changes.
It just updates the username observable with a new value.
Could be written as:
onChanged: (val) => fx.username.value = val
ElevatedButton (a "Submit" button)
onPressed function can change between null and a function
null disables the button (only way to do so in Flutter)
a function here will enable the button
class FormObxPage extends StatelessWidget {
const FormObxPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
FormX fx = Get.put(FormX()); // controller
return Scaffold(
appBar: AppBar(
title: const Text('Form Validation'),
),
body: SafeArea(
child: Container(
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(
() {
print('rebuild TextFormField ${fx.errorText.value}');
return TextFormField(
onChanged: fx.usernameChanged, // controller func
decoration: InputDecoration(
labelText: 'Username',
errorText: fx.errorText.value // obs
)
);
},
),
Obx(
() => ElevatedButton(
child: const Text('Submit'),
onPressed: fx.submitFunc.value, // obs
),
)
],
),
),
),
);
}
}
GetX Controller
Explanation / breakdown below
class FormX extends GetxController {
RxString username = RxString('');
RxnString errorText = RxnString(null);
Rxn<Function()> submitFunc = Rxn<Function()>(null);
#override
void onInit() {
super.onInit();
debounce<String>(username, validations, time: const Duration(milliseconds: 500));
}
void validations(String val) async {
errorText.value = null; // reset validation errors to nothing
submitFunc.value = null; // disable submit while validating
if (val.isNotEmpty) {
if (lengthOK(val) && await available(val)) {
print('All validations passed, enable submit btn...');
submitFunc.value = submitFunction();
errorText.value = null;
}
}
}
bool lengthOK(String val, {int minLen = 5}) {
if (val.length < minLen) {
errorText.value = 'min. 5 chars';
return false;
}
return true;
}
Future<bool> available(String val) async {
print('Query availability of: $val');
await Future.delayed(
const Duration(seconds: 1),
() => print('Available query returned')
);
if (val == "Sylvester") {
errorText.value = 'Name Taken';
return false;
}
return true;
}
void usernameChanged(String val) {
username.value = val;
}
Future<bool> Function() submitFunction() {
return () async {
print('Make database call to create ${username.value} account');
await Future.delayed(const Duration(seconds: 1), () => print('User account created'));
return true;
};
}
}
Observables
Starting with the three observables...
RxString username = RxString('');
RxnString errorText = RxnString(null);
Rxn<Function()> submitFunc = Rxn<Function()>(null);
username will hold whatever was last input into the TextFormField.
errorText is instantiated with null initial value so the username field is not "invalid" to begin with. If not null (even empty string), TextFormField will be rendered red to signify invalid input. When a non-valid input is in the field, we'll show an error message. (min. 5 chars in example:)
submitFunc is an observable for holding a submit button function or null, since functions in Dart are actually objects, this is fine. The null value initial assignment will disable the button.
onInit
The debounce worker calls the validations function 500ms after changes to the username observable end.
validations will receive username.value as its argument.
More on workers.
Validations
Inside validations function we put any types of validation we want to run: minimum length, bad characters, name already taken, names we personally dislike due to childhood bullies, etc.
For added realism, the available() function is async. Commonly this would query a database to check username availability so in this example, there's a fake 1 second delay before returning this validation check.
submitFunction() returns a function which will replace the null value in submitFunc observable when we're satisfied the form has valid inputs and we allow the user to proceed.
A little more realistic, we'd prob. expect some return value from the submit button function, so we could have the button function return a future bool:
Future<bool> Function() submitFunction() {
return () async {
print('Make database call to create ${username.value} account');
await Future.delayed(Duration(seconds: 1), () => print('User account created'));
return true;
};
}
GetX is not the solution for everything but it has some few utility methods which can help you achieve what you want. For example you can use a validator along with SnackBar for final check. Here is a code snippet that might help you understand the basics.
TextFormField(
controller: emailController,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (!GetUtils.isEmail(value))
return "Email is not valid";
else
return null;
},
),
GetUtils has few handy methods for quick validations and you will have to explore each method to see if it fits your need.

How to create buttons with for loop in a column in Flutter?

I need to implement lists of buttons in a column depending on the data entry. So, for that I have to use for loop. Each button requires two entires id, text. I can make it with List. But it accepts only string value not the integer.
This is the code I tried.
code
Widget getTextWidgets(List<String> strings)
{
List<Widget> list = new List<Widget>();
for(var i = 0; i < strings.length; i++){
list.add(new ButtonForHome(
lable: strings[i],
onPressed: (){},
));
}
return new Column(children: list);
}
I want to put id in onPressed event. How can I implement in the Flutter?
You should use Listview instead of Column
SAMPLE CODE
getTextWidgets() {
return ListView.builder(
itemCount: yourList.length,
itemBuilder: (context, itemIndex) {
return MaterialButton(
child: Text(yourList[itemIndex]),
onPressed: () {
debugPrint('Clicked index $itemIndex');
});
});
}
Now your question
I want to put id in onPressed event. How can I implement in the Flutter?
You can create a POJO class like this
class DataModel{
String name;
int id;
DataModel(this.name, this.id);
}
Now create list of your POJO class
List<DataModel> list= List<DataModel>();
Now add data in your list like this
list.add(DataModel("name", 1));
list.add(DataModel("name", 2));
list.add(DataModel("name", 3));
Now you can use it like this way
getTextWidgets() {
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, itemIndex) {
return MaterialButton(
child: Text(list[itemIndex].name),
onPressed: () {
debugPrint('Clicked item name '+list[itemIndex].name);
debugPrint('Clicked item ID '+list[itemIndex].id.toString());
});
});
}
Nilesh Rathod has indeed given the descriptive answer for the same. In flutter there is also, a way to achieve this, which is quite similar to POJO class, is
To create own widget and specify the fields needs to be passed when we are using the widget
Add the widget to the list, with the data specified for passing
You can track, the id, by pressing itself also
I can clearly see that, you have created your own widget named as ButtonForHome, which takes in label for now. What you can do is, to make your widget takes in two argument, and you can do it like this:
class ButtonForHome extends StatelessWidget {
final String label;
final int id; // this will save your day
// #required will not let user to skip the specified key to be left null
ButtonForHome({#required this.label, #required this.id});
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
color: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),
child: Text(this.label),
onPressed: () => print(this.id) // Here you print your id using your button only
)
);
}
}
Now creating your button with a list, or adding via list
You can do it via ListView.builder()
You can do it via your way, i.e., List<Widget>.add()
I am gonna show the solution in your way only:
Widget getTextWidgets(List<String> strings){
List<Widget> list = new List<Widget>();
for(var i = 0; i < strings.length; i++){
list.add(ButtonForHome(
id: i, // passing the i value only, for clear int values
label: strings[i]
));
}
return Column(children: list);
}
With the new flutter in place, you don't need to do new every time while defining a widget. It understands now, so no need of const, new at all
So, wherever you populate your getTextWidget, it will show up the Widgte ButtonForHome, which has unique id, and label. Now the ButtonForHome, prints the id of that particular widget which was passed uniquely. So now, you can see your result happening.
I hope this is what you were looking for. Try it, and let me know.

Trouble implementing contextual aware icon button in flutter

I have a TextField and an IconButton in a row like so.
I would like the IconButton to be enabled only when there is text in the TextField. I am using the provider package for state management.
Here is the ChangeNotifier implementation.
class ChatMessagesProvider with ChangeNotifier{
List<ChatMessage> chatMessages = <ChatMessage>[];
bool messageTyped = false;
ChatMessagesProvider(this.chatMessages);
void newMessage(String textMessage){
ChatMessage message = ChatMessage(textMessage);
this.chatMessages.add(message);
notifyListeners();
}
int messageCount() => chatMessages.length;
void updateMessageTyped(bool typed){
this.messageTyped = typed;
// notifyListeners(); Un-comennting this makes the Text disappear everytime I type something on the text field
}
}
Here is the actual widget:
class TextCompose extends StatelessWidget {
final TextEditingController _composeTextEditingController = new TextEditingController();
TextCompose(this.chatMessagesProvider);
final ChatMessagesProvider chatMessagesProvider;
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Flexible(
child: new TextField(
controller: _composeTextEditingController,
onSubmitted: (String text) {
_onMessageSubmitted(text, context);
},
onChanged: (String text){
if(text.length > 0){
chatMessagesProvider.updateMessageTyped(true);
print(text);
}
else{
chatMessagesProvider.updateMessageTyped(false);
print("No text typed");
}
},
decoration: new InputDecoration.collapsed(
hintText: "Enter message"
),
),
),
new Container(
margin: new EdgeInsets.all(8.0),
child: new IconButton(
color: Theme.of(context).accentColor,
icon: new Icon(Icons.send),
disabledColor: Colors.grey,
onPressed:chatMessagesProvider.messageTyped // This dosen't work
? () => _onMessageSubmitted(_composeTextEditingController.text, context)
: null,
),
)
],
),
);
}
void _onMessageSubmitted(String text, BuildContext context){
if(chatMessagesProvider.messageTyped) { // This works fine.
// clear the message compose text box
_composeTextEditingController.clear();
// add the message to provider.
chatMessagesProvider.newMessage(text);
// set the message typed to false
chatMessagesProvider.messageTyped = false;
}
I am using messageTyped from ChatMessageProvider to check to see if there is any text in the TextField. It seems to work fine when I check it in the _onMessageSubmitted method but not when I check its value in the onPressed property for the IconButton.
I know this because I can see the IconButton remains disabled(colour doesn't change from grey) when I type text, whereas I can hit the submit button on the virtual keyboard and the text is cleared from the TextField(as per call to _composeTextEditingController.clear())
Question:
why does chatMessagesProvider.messageTyped return the right value when called from the _onMessageSubmitted but not when it is called from the onPrssed attribute in the IconButton?
How would I debug something like this in Flutter, I would really like to drop a breakpoint in onPressedAttribute and see the value for chatMessagesProvider.messageTyped
Let me know if you need to see any more of my code.
onPressed:chatMessagesProvider.messageTyped this line is being executed during widget build time so it is always default value and it will never get refreshed until unless you rebuild the widget using notify listener or stateful widget.
Store the currently being typed message in your provider and make your send button enable/disable depends on whether currently being typed message is empty or not.
You say you are using 'provider_package' but you actually have no Provider in your layout. Instead you have a custom built ChangeNotifier with no listeners - you are indeed calling notifyListeners() but there are actually no listeners, so no rebuild is being triggered. A rebuild is needed in order for the button to change its onPressed function reference and implicitly its color.
As for debugging, you can set a breakpoint on the line with onPressed, but it will only be hit during a rebuild.
The most important thing to understand is that the function reference you give to onPressed will be invoked correctly, but a rebuild is needed for the widget to change visually.
Although your current ChangeNotifier implementation does not make much sense, simply wrapping your calls to updateMessageTyped within setState should solve the visual problem - and your breakpoint will also be hit after each typed/deleted character.
The simplest solution you can, first of all, make your widget StatefulWidget.
You need a boolean inside State class:
bool hasText = false;
Then create initState:
#override
void initState() {
_composeTextEditingController.addListener(() {
if (_composeTextEditingController.text.isNotEmpty) {
setState(() {
hasText = true;
});
} else {
setState(() {
hasText = false;
});
}
});
super.initState();
}
Also don't forget to dispose:
#override
void dispose() {
_composeTextEditingController.dispose();
super.dispose();
}
And finally your build method:
Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _composeTextEditingController,
)),
if (hasText) IconButton(icon: Icon(Icons.send), onPressed: () {})
],
),

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((){});
},
)