how to rebuild dialog Widget on changing bool variable state? - flutter

im trying to submit form on Dialog and i have a DateTimePicker button and need to make a validation on it also before submitting , what i want to do is showing a text error in case no date picked by changing my own variable "isValid" to false but the UI is not updating , i have to close the dialog and reopen it to see the error text even though i wrapped my column with a StatefulBuilder
my dialog photo here
here is my code
StatefulBuilder(builder: (context, StateSetter setState) {
return isValid == false
? Column(
children: [
ElevatedButton(
onPressed: () {
DateTimePicker(context)
.then((value) => setState(() {
_appointmentDateTime = value;
}));
},
child: Text(getTimeDate())),
Text(
'error',
style: TextStyle(
color: Colors.red, fontSize: 10),
),
],
)
: Column(
children: [
ElevatedButton(
onPressed: () {
DateTimePicker(context)
.then((value) => setState(() {
_appointmentDateTime = value;
}));
},
child: Text(getTimeDate())),
],
);
})
Validating form + toggling the isValid Value is working fine
OutlinedButton(
onPressed: () async {
if (_formKey.currentState.validate() &&
_appointmentDateTime != null) {
String date = DateFormat('yyyy-MM-dd hh:mm')
.format(_appointmentDateTime);
var appointment = Appointment(
patientName: widget.patient.name,
date: date,
hospital: _hospitalController.text,
await FirebaseApi.addPatientAppointment(
widget.patient.id, appointment);
print('Appointment Created ');
_formKey.currentState.reset();
setState(() {
translator = null;
_appointmentDateTime = null;
});
Navigator.pop(context);
}
else {
setState(() {
isValid = !isValid;
});
}
},
child: Text('Add Appointment')),

It can get confusing when writing the code like this when dealing with Dialogs. The setState you are using in the OutlinedButton is not the same as the setState used in the StatefulBuilder. You need to enclose your OutlinedButton inside the StatefulBuilder too. If you ever use a StatefulBuilder inside a stateful widget, it is better to use a different name like e.g setDialogState.
It is even better to create a separate stateful widget class just for your Dialog contents and pass the formKey and anything else than using a StatefulBuilder in this case to avoid confusion.

Related

Showing a button to the user based on the data entered in the TextFormField

final TextEditingController _weight = TextEditingController();
if (_weight.text.contains(RegExp(r'[0-9]')))
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: BMIButton(
onpressed: () {
Navigator.push(
context,
PageTransition(
type: PageTransitionType.rightToLeft,
child: BMIHeight(),
inheritTheme: true,
ctx: context),
);
},
))
I'm trying to show an OutlinedButton when the user enters some data into the textFormField. When I enter a value in the TextFormField and confirm, it doesn't show a button, but when I hot reload it, it sees that value and shows the button.
Try to add listener and call setState to update ui
late final TextEditingController _weight = TextEditingController()
..addListener(() {
setState(() {});
});
And override the dispose method and depose this controller.
This means you need just to update the state of your widget, first make sure this inside a StatefulWidget, then add a SetState(() {}) on the end of that method:
if (_weight.text.contains(RegExp(r'[0-9]')))
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: BMIButton(
onpressed: () {
Navigator.push(
context,
PageTransition(
type: PageTransitionType.rightToLeft,
child: BMIHeight(),
inheritTheme: true,
ctx: context),
);
},
))
//...
setState(() {}) // add this
TextEditingController is implementing Listenable. You can exploit that to build part of your UI conditionally.
Moreover, TextEditingController must be correctly initialized and disposed of. You can do that smoothly with flutter_hooks.
You'd obtain this concise result:
class MyWidget extends HookWidget {
const MyWidget({super.key});
#override
Widget build(BuildContext context) {
final textController = useTextEditingController();
return Column( // example, I'm not sure of what you've got there
children: [
TextField(controller: textController), // Your Text Field
ValueListenableBuilder(
valueListenable: textController,
builder: (context, value, child) {
return value.text.contains(...)
? OutlinedButton( // Your Button
onPressed: () {
// .. do stuff
},
child: const Text('I am ready to be pressed'),
)
: const SizedBox.shrink(); // This is empty, but you can render whatever
},
)
],
);
}
}
Your code that checks if the TextFormField is populated needs to be inside the build function of a stateful widget.
To trigger the state update, listen to changes on the TextFormField, inside the function set the state of some variable that you can check.
Add this to the initState method:
_weight.addListener(() {
setState(() {
_inputText = _weight.text; // Create this variable inside the state class
});
});
And change your if statement as follows:
if (_inputText.contains(RegExp(r'[0-9]')))
Method 2
You could also Wrap your Widget with a Visibility Widget:
Visibility(
visible: RegExp(r'[0-9]').hasMatch(_inputText),
child: [YOUR WIDGET],
)
This still needs the listener with the setState call.

How to wait for value before calling method Flutter/Dart

I am having a pretty hard time with certain actions in flutter. I currently have a method in an outside class that updates a db that my widget relies on for displaying info. I am correctly updating the values in the db and updating the UI correctly. BUT I am having a hard time getting an input first, THEN having that method function. I have tried having it all in the same body and no dice, I have tried to have the addStock method show the input and does not work. The only thing that has been a ban-aid has been to use Navigator.push to the screen again or using a time delayed. Both have produced undesired consequences. I have also tried having the addStock method inside the displayAmountToADD on pressing okay and does not update UI.
//a button inside the UI
onPressed: () async {
displayAmountToAdd(context, index);
setState(() {});
},
....
Future<void> displayAmountToAdd(
BuildContext context,
int index,
) async {
final _textFieldController = TextEditingController();
double materialKG = 0;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Enter amount to add'),
content: Row(
children: <Widget>[
Expanded(
child: TextField(
onChanged: (materialQuanity) {
materialKG = double.parse(materialQuanity);
},
controller: _textFieldController,
decoration: InputDecoration(hintText: "KG"),
),
),
],
),
actions: <Widget>[
TextButton(
child: Text('OK'),
onPressed: () {
materialKG = double.parse(_textFieldController.text);
addStock(context, mapM[index]['quanity'], mapM[index]['name'],
materialKG);
Navigator.pop(context);
},
),
TextButton(
child: Text("Cancel"),
onPressed: () {
Navigator.pop(context);
})
],
);
},
);
//return Future.delayed(Duration(seconds: 4),()=>materialKG); //TRYING TO AVOID THIS
}
//outside the ui file
addStock(
BuildContext context,
double currentQuanity,
String name,
double amountToAdd
) async {
//final db = await database;
double newStock;
late double materialKG;
newStock=currentQuanity+amountToAdd;
await db.rawUpdate(
'UPDATE materials SET quanity = $newStock WHERE name = "$name" ');
mapM = await db.query('materials'); //update values
//the following is only because setState is not working properly on other screen
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("Added $amountToAdd KG to $name"),
));
}
displayAmountToAdd and showDialog method are future. Use await before theses to hold method to finish.
A sample example:
const oneSecond = Duration(seconds: 1);
// ยทยทยท
Future<void> printWithDelay(String message) async {
await Future.delayed(oneSecond);
print(message);
}
Learn more about async-await.

Only update UI when there are changes in the server, using streambuilder

I am using streambuilder to check whether a new order is placed or not.
I am checking the order status, if the order status is unknown I want to show a pop up, which works fine. but if i don't select an option to update the order status, streambuilder refreshes after a few seconds, and show another pop up on top of it.
Get Orders Function:
Future<Orders> getOrders() async {
String bsid = widget.bsid;
try {
Map<String, dynamic> body = {
"bsid": bsid,
};
http.Response response = await http.post(
Uri.parse(
"**API HERE**"),
body: body);
Map<String, dynamic> mapData = json.decode(response.body);
Orders myOrders;
print(response.body);
if (response.statusCode == 200) {
print("Success");
myOrders = Orders.fromJson(mapData);
}
return myOrders;
} catch (e) {}
}
Here's the stream function:
Stream<Orders> getOrdersStrem(Duration refreshTime) async* {
while (true) {
await Future.delayed(refreshTime);
yield await getOrders();
}}
StreamBuilder:
StreamBuilder<Orders>(
stream: getOrdersStrem(
Duration(
seconds: 2,
),
),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
CircularProgressIndicator.adaptive(),
);
}
var orders = snapshot.data.statedatas;
return ListView.builder(
itemCount: orders.length,
itemBuilder: (BuildContext context, int index) {
var orderResponse =
snapshot.data.statedatas[index].strAccept;
print(orderResponse);
if (orderResponse == "0") {
print("order status unknown");
Future.delayed(Duration.zero, () {
_playFile();
showCupertinoDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Center(
child: Text(
"#${orders[index].ordrAutoid}",
),
),
content: Row(
children: [
SizedBox(
width: 120,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty
.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(
MaterialState.pressed))
return Colors.black;
return Colors
.green; // Use the component's default.
},
),
),
onPressed: () async {
_stopFile();
Navigator.pop(context);
await changeOrderStatus(
orders[index].orid, "accept");
// setState(() {});
},
child: Text('Accept'),
),
),
SizedBox(
width: 15,
),
SizedBox(
width: 120,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty
.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(
MaterialState.pressed))
return Colors.black;
return Colors
.red; // Use the component's default.
},
),
),
onPressed: () async {
_stopFile();
Navigator.pop(context);
await changeOrderStatus(
orders[index].orid, "Reject");
// setState(() {});
},
child: Text('Reject'),
),
),
// TextButton(
// onPressed: () async {
// _stopFile();
// Navigator.pop(context);
// await changeOrderStatus(
// orders[index].orid, "reject");
// },
// child: Text('reject'),
// ),
],
),
),
);
}).then((value) {
_stopFile();
print("ENDING");
});
}
return Container();
Create a variable to check for the last known order status, outside your if statement, and when a new value comes, compare it to the old value first, then do the if statement logic.
//This is outside the stream builder:
String orderResponseCheck = "";
.
.
.
//This is inside your streambuidler, if the orderResponseCheck is still equal to "", the if statement will be executed,
//and the value of orderResponse wil be assigned to it. This will only show the alert dialog if the orderResponse status changes from the one that previously triggered it.
var orderResponse =snapshot.data.statedatas[index].strAccept;
print(orderResponse);
if (orderResponseCheck != orderResponse && orderResponse == "0") {
orderResponseCheck = orderResponse;
.
.
.
//logic same as before
You shouldn't call showCupertinoDialog (and probably _playFile()) from your build method. Wrapping it with Future.delayed(Duration.zero, () { ... }) was probably a workaround for an error that was given by the framework.
The build method can get executed multiple times. You probably want a way to run _playFile and show the dialog that isn't depending on the UI. I don't think StreamBuilder is the right solution for this.
You could use a StatefulWidget and execute listen on a stream from the initState method. initState will only be called once.
From what I'm reading, you're querying your API every two seconds.
Every time your API answers, you're pushing the new datas to your StreamBuilder, which explains why you're having multiple pop-ups are stacking.
One simple solution to your problem would be to have a boolean set to true when the dialog is displayed to avoid showing it multiple times.
bool isDialogShowing = false;
...
if (orderResponse == "0" && !isDialogShowing) {
isDialogShowing = true;
...
But there are a few mistakes in your code that you should avoid like :
Infinite loops
Querying your API multiple times automatically (it could DDOS your service if plenty of users are using your app at the same time)
Showing your Dialog in a ListView builder

Flutter TextField calls onSubmitted unexpectedly

My case is I have a widget with TextField used for search. When I type something in TextField the cross icon become visible in suffixIcon (to clear). I do not do search and just click the cross icon to clear the entered input but onSubmitted is called and search executed!!! But I don't need it! I do not submit the text input, I cancel it!!
final searchClear = ValueNotifier(false);
final searchController = TextEditingController();
// in initState method:
searchController.addListener(() {
searchClear.value = searchController.text.isNotEmpty;
});
// in build method:
TextField(
...
controller: searchController,
suffixIcon: ValueListenableBuilder<bool>(
valueListenable: searchClear,
builder: (_,visible,child) {
return Visibility(
visible: visible,
child:child,
);
},
child: InkWell(
child: Icon(Icons.close),
onTap: () {
searchController.clear();
searchFocus.unfocus();
}
),
),
onSubmitted: (value) {
if(value.isEmpty) {
FocusScope.of(context).requestFocus(searchFocus);
} else {
widget.search(value);
}
}
),
P.S. Any ideas to work around this?

Using Flutter Checkbox() and obtaining initial value from Firestore

I'm no experienced programmer and I could find no guidance, hence this question. I have a working solution but not sure if this is good practice.
I am using the widget Checkbox() in a Form(). Widgets like TextFormField() and DateTimeField() have a parameter called 'initialValue'. Checkbox() does not.
For TextFormField() and DateTimeField() I obtained the initialValue by:
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UnitDetails>(
stream: DatabaseServices(uid: user.userUid, unitUid: widget.unitUid)
.unitByDocumentID,
builder: (context, unitDetails) {
if (!unitDetails.hasData) return Loading();
return Scaffold(
etc
The Checkbox(value: residentialUnit,) can not have its initial value set inside the builder:. The parameter 'value:' needs to be set true or false before the builder: ie before the value is obtained from Firestore! The way I solved this is by using initState(). An extra call to Firestore and more code for this one input widget.
#override
void initState() {
super.initState();
Firestore.instance
.collection("units")
.document(widget.unitUid)
.snapshots()
.listen((snapshot) {
residentialUnit = snapshot.data['unitResidential'];
});
}
Is there a better way?
I think you can solve your problem with the following answer (using FormField).
Checkbox form validation
Following is a sample code.
FormField(
initialValue: userProfile.agreement,
builder: (state) {
return Column(
children: [
Row(
children: [
Checkbox(
activeColor: Colors.blue,
value: state.value,
onChanged:(value) {
setState(() {
state.didChange(value);
});
}
),
Expanded(child: Text('Sample checkbox')),
],
),
Text(
state.errorText ?? '',
style: TextStyle(
color: Theme.of(context).errorColor,
),
)
],
);
},
validator: (val) {
print('VAL: $val');
if (!val) {
return 'You need to accept terms';
} else {
return null;
}
},
)