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

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

Related

Clicking the button does not show the loader in Flutter

I need to add a loader to the button. That is, when you click on the Save button, you need to show the CircularProgressIndicator. I wanted to implement this through Bloc, because if I do this through setState (() {}), then I will have to rebuild the widget and the new added data on the page will disappear. Therefore, I want to do it through Bloc. I created a variable there and assign values ​​to it, the values ​​change but the loader does not update, tell me how can I make the loader show?
return BlocBuilder<EditPhotoCubit, EditPhotoState>(
builder: (context, state) {
return SizedBox(
height: size.height,
width: size.width,
child: _child(size, topPadding, context, cubitEditPhoto,
mainState.charging),
);
});
}
return const SizedBox();
});
...
SizedBox(
width: 148,
child: cubitEditPhoto.isLoading
? const CircularProgressIndicator(
color: constants.Colors.purpleMain)
: DefaultButtonGlow(
text: 'Save',
color:Colors.purpleMain,
shadowColor: Colors.purpleMain,
textStyle: Styles.buttonTextStyle,
onPressed: () async {
cubitEditPhoto.isLoading = true;
await cubitEditPhoto
.uploadImage(widget.chargingId)
.then((value) {
cubitEditPhoto.isLoading = false;
});
},
),
),
cubit
class EditPhotoCubit extends Cubit<EditPhotoState> {
EditPhotoCubit() : super(EditPhotoInitial());
bool isLoading = false;
}

How to render image from Firebase without rebuilding?

I'm trying to implement profile image picking in my flutter app using Firebase Storage. I use image_picker to get the image and upload it to Firebase, get the download link and add the download link to the imgsrc field in the cloud firestore, from where I can render the NetworkImage.
Center(
child: Stack(
children: [
buildImage(),
Positioned(
bottom: 5,
right: 5,
child: GestureDetector(
onTap: showPhotoAlertDialog,
child: buildEditIcon(Color(0xff407bff))),
),
],
),
),
How can I get the default Icons.person kind image for when the user has no profile image, and get the image from the database otherwise?
The code I'm using right now is as follows:
Widget buildImage() {
return CircleAvatar(
backgroundImage: NetworkImage(loggedInUser.imgsrc ??
'https://th.bing.com/th/id/R.945f33b643f2ceffcdae90fb57c61854?rik=XcI0SYBgSefoCA&riu=http%3a%2f%2fgetdrawings.com%2ffree-icon-bw%2fanonymous-avatar-icon-19.png&ehk=5n%2buJG66CeLQZsmhaMt8gag5rXuM3TdebAL6W35K1E4%3d&risl=&pid=ImgRaw&r=0'),
backgroundColor: Colors.grey[350],
radius: 100,
);
}
I created an Alert Dialog widget to choose whether to choose the image from camera or from the gallery.
showPhotoAlertDialog() {
AlertDialog alert = AlertDialog(
title: Text("Upload from"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
imageFromCamera()
.then((value) => uploadFile())
.whenComplete(() => postSource());
setState(() {}); ----->
},
child: Text("Upload from camera"),
),
TextButton(
onPressed: () {
imageFromGallery().then((value) => uploadFile());
postSource();
setState(() {});
},
child: Text("Upload from gallery"),
)
],
),
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
To upload the image to storage and post the source to cloud firestore, I use the following methods:
Future uploadFile() async {
if (file == null) return;
final fileName = path.basename(file!.path);
final destination = 'files/$fileName';
task = FirebaseApi.uploadFile(destination, file!);
setState(() {});
if (task == null) return;
final snapshot = await task!.whenComplete(() {});
urlDownload = await snapshot.ref.getDownloadURL();
print('Download-Link: $urlDownload');
}
postSource() async {
FirebaseFirestore firebaseFirestore = FirebaseFirestore.instance;
await firebaseFirestore
.collection("users")
.doc(user?.uid)
.update({'imgsrc': urlDownload});
}
The link gets uploaded properly and I'm able to get the link in my NetworkImage, but it doesn't get rendered immediately. I have to close the parent drawer and open it again to get it. I call setState(){} as well after posting the source, but it doesn't make any difference. How can I get the image without having to close and open the drawer?
Any help would be appreciated!
Thanks
You also have to update image in model class or in this imgsrc also just add this line above setState in onPressed of TextButton.
loggedInUser.imgsrc = urlDownload;

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.

how to rebuild dialog Widget on changing bool variable state?

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.

Flutter: How can I prevent Future for snapshot from firing unnecessarily

I'm working on a program that displays a list from an SQFlite table. The future for the snapshot list is being fired for no reason that I can determine. It fires approximately 3 times more than is needed. The only times that it needs to fire are (1) the first time that program activates, and (2) when it returns from the update screen which can create, read, update, and delete. Consequently, I set a flag on return from that screen to indicate that the snapshot needs to be refreshed. Then in the function that selects the data, I check if the flag is set, and only then do I select the table.
Just running the program now for some additions and deletions resulted in the following for the select for the snapshot:
"I/flutter (24769): Fetched = false, Fetch attempts = 20, Fetched = 7"
This indicates that only 7 selects were needed, but 20 were requested.
Can someone advise me the correct way to prevent the Future firing when not necessary?
Relevant code is below:
body: Container(
padding: EdgeInsets.all(16.0),
child: FutureBuilder<List<Map>>(
future: _fetchDataFromDb(),
builder: (context, AsyncSnapshot<List<Map>> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (!snapshot.hasError && snapshot.hasData) {
return ListView.builder(
itemCount: snapshot == null ? 0 : snapshot.data.length,
itemBuilder: (context, index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ListTile(
leading: (IconButton /* Edit */ (
color: Colors.blue,
icon: Icon(Icons.edit),
onPressed: () => _showEditScreen(
Crud.eUpdate,
snapshot.data[index]))),
title:
Text(snapshot.data[index]['title']),
subtitle:
Text(snapshot.data[index]['detail']),
onLongPress: () => _showEditScreen(
Crud.eRead, snapshot.data[index]),
trailing: (IconButton(
color: Colors.red,
icon: Icon(Icons.delete),
onPressed: () => _showEditScreen(
Crud.eDelete,
snapshot.data[index])))),
]);
});
}
}
})),
Future<List<Map>> _fetchDataFromDb() async {
bool tfFetched = false;
_iFetchAttempts++;
if (_tfGetData) {
print("Fetching data");
_snapshot = await _dbHelper.getNoteRecs();
tfFetched = true;
_tfGetData = false;
_iFetched++;
setState(() => _iCount = _snapshot.length);
}
print(
"Fetched = $tfFetched, Fetch attempts = $_iFetchAttempts, Fetched = $_iFetched");
return _snapshot;
}
void _showEditScreen(Crud eCrud, data) async {
try {
NoteRec noteRec = data == null
? null
: NoteRec(data['id'], data['title'], data['detail']);
await Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
NoteEntry(g_crud: eCrud, g_noteRec: noteRec)));
_tfGetData = true; // SET FLAG TO INDICATE SELECT IS REQUIRED
} catch (error) {
print("Error on navigation = ${error.toString()}");
}
}
After some research, I believe that the answer to this question is that the selection of data from a database or elsewhere should be separate from the rebuild. The rebuild is given this data as part of the rebuild, but it is not selected as part of the rebuild.