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

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.

Related

Firestore get length data disappear in flutter stream

I make a chat using firebase firestore.
So. I tried express not read count in the Chat list.
But, Initially the number appears, but it changes to null data.
I don't know Why data chage null data?
body: StreamBuilder(
stream: FirebaseFirestore.instance
.collection('chatRooms')
.where('emails', arrayContainsAny: [user?.email]).snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
var chatLists = snapshot.data?.docs;
if (snapshot.hasError) {
return Text('Something error...');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text('is Loading...');
}
return ListView.builder(
itemCount: chatLists?.length,
itemBuilder: (context, index) {
if (chatLists?[index]['currentMsg'] != null &&
chatLists?[index]['currentMsg'] != "") {
var list = List.from(chatLists?[index]['members']);
var member = '';
if (loginUser['userName'] != null) {
for (int i = 0; i < list.length; i++) {
if (list[i] != loginUser['userName']) {
member += list[i];
}
}
}
return ListTile(
title: Row(
children: [
Text(member),
const SizedBox(
width: 20.0,
),
ChatLength(docId: chatLists![index].id, uid: user!.uid),
],
),
subtitle: SizedBox(
height: 40.0,
child: Text(
chatLists[index]['currentMsg'],
overflow: TextOverflow.ellipsis,
)),
trailing: Text(
tmFormmater(chatLists?[index]['currentTm']),
style: const TextStyle(
color: Color(0xff999999),
),
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(
docId: chatLists![index].id,
title: member,
),
)),
);
} else {
return Container();
}
},
);
return Container();
},
),
class ChatLength extends StatelessWidget {
const ChatLength({super.key, required this.docId, required this.uid});
final String docId;
final String uid;
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection('message_read')
.doc(docId)
.collection('message')
.where('userId', isNotEqualTo: uid)
.where('read', isEqualTo: false)
.snapshots(),
builder: (context, snapshot) {
print(snapshot.data?.size);
if (snapshot.data?.size != null) {
return Container(
child: Text('${snapshot.data?.size}'),
);
} else {
return Container();
}
},
);
}
}
===== Debug console ====
flutter: 1
flutter: null // Data changes to null immediately
message_read collection structure
message_read -> documentId -> message -> documentId -> field(userId(chat writer), read)
I'm trying get a snapshot data from firestore.
And I put the imported data into the text.
But Data changes to null immediately.
Since you use snapshots() the code is not just reading the data once, but then continues listening for changes to the data. So the two values you see mean that the snapshots() stream was triggered twice, the first time with a single message - and the second time without any messages.
My educated guess is that your code changes the read value of the document after it gets it, since it has now displayed that message to the user. But doing so means the document no longer meets the criteria of your query, so the snapshots stream gets a new event without that message in it.
Consider using a different mechanism for the query, for example I usually use a timestamp to determine what messages to show. Step by step:
Ensure each message document has a timestamp field.
For each user store (either in the database or in local storage of the app) when they last started the app.
Then request from the database the messages since they last started the app.
Make sure to start the query before you update the timestamp value, otherwise you'll never get any results.

How to display all data in array?

I'm trying to display data from an array in firestore. I displayed it, but only [0] in the array is showing. I'm trying to get all the data in the array to show.
builder: (_, AsyncSnapshot<List<DocumentSnapshot>> snapshot){
if(snapshot.hasData){
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: ((_, index) {
List<Widget> tiles = [];
for (Map post in snapshot.data![index]['posts']) {
tiles.add(
Expanded(
child: Container(
margin: EdgeInsets.all(2),
padding: EdgeInsets.all(1),
decoration: BoxDecoration(border: Border.all(color:Colors.black)),
child: Center(
child: ListTile(
title: Text(post['postText'], style: TextStyle(color: Colors.white),),
subtitle: Text(post['fromUser'], style: TextStyle(color: Colors.white),),
),
),
),
)
);
}
return Expanded(
child: ListView(
children: tiles,
),
);
}),
);
}
else{
return Center(child: CircularProgressIndicator(),);
}
},
enter image description here
Edit
To answer your qn about newest to oldest:
I suggest you put a FieldValue.timestamp field in your group chat documents! Then, you can order them like this:
Future<List<DocumentSnapshot>> getDoc(groupID) async {
var firestore = FirebaseFirestore.instance;
QuerySnapshot qn = await firestore.collection('groups')
.where('groupChatId', isEqualTo: groupID)
.orderBy('timestamp', descending: true) // <- Here!
.get();
return qn.docs;
}
(All of that I copied by hand, since you hadn't provided this code as text, as I asked you to!... 😆)
If you don't have a timestamp field, there is a way to still find out when a document was created... but I don't know how. Plus, in this case, I guess you want the time a certain FIELD was created in the document...! I don't know if that's possible. In fact, for that you'll probably have to do:
List<Map> posts = snapshot.data![index]['posts'];
// Sort list according to the 'date' field in each Map in the list:
posts.sort((mapA, mapB){
return mapA['date'].compareTo(mapB['date']);
});
// Then you'll use posts in your for-loop instead of snapshot.data![index]['posts']:
for (Map post in posts) {
tiles.add( /*etc*/);
}
Btw, if you want it to update when new messages come in, you can do like this:
import 'dart:async';
// Put the below in the State of a StatefullWidget:
StreamSubscription<QuerySnapshot<Map<String, dynamic>>>? qn;
List<DocumentSnapshot>? eventDocs;
void getDocStream(groupID) async {
var firestore = FirebaseFirestore.instance;
qn = firestore.collection('groups')
.where('groupChatId', isEqualTo: groupID)
.orderBy('timestamp', descending: true)
.snapshots().listen((event) {
// Put here everything you want to happen when new things happen in the stream!
// For example:
setState(() {
eventDocs = event.docs;
});
// Now, you can use eventDocs instead of getDoc(groupID), as you did before.
// Just remember that it will be null at first!
});
}
#override
void dispose() {
if (qn != null) qn!.cancel(); // This is to prevent the stream from going on, after you've left the page or even closed the app...
super.dispose();
}
Old answer:
But you're telling it to display only post [0]!...
If there are more posts in each document, and you want to display all of them, you need to make a for-loop or something. For example:
itemBuilder: ((_, index) {
List<Widget> tiles = [];
for (Map post in snapshot.data![index]['posts']) {
tiles.add(
ListTile(
title: Text(post['postText']),
subtitle: Text(post['fromUser']),
));
}
return Expanded(
child: Column(
children: tiles,
),
);
}),
And btw... Next time you ask a qn, plz paste your code as text rather than an image! So that we can copy-paste it into our answer, rather than having to retype it from the image. It's so easy to make a mistake and then you get an error coz we didn't copy it right.
try this
title: Text(snapshot.data![index]['posts']['postText']),

Flutter Blue Read Values from 2 Characteristics with notify

I'm a beginner in flutter and want to program a app with Bluetooth LE. I use flutter_blue. My App works if i only want to read the value of one characteristic. Now i need to read a second characteristic and that is the problem. I'm using a example from the esp32 dust sensor. The main is similar to the flutter blue example. The step to the next site is work with the following code:
StreamBuilder<List<ScanResult>>(
stream: FlutterBlue.instance.scanResults,
initialData: [],
builder: (c, snapshot) => Column(
children: snapshot.data
.map(
(r) => ScanResultTile(
result: r,
onTap: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
r.device.connect();
return SensorPage(device: r.device);
})),
),
That works ok. At the Sensor-Page there is the code for the Page which starts with:
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
appBar: AppBar(
title: Text(' Connect'),
),
body: Container(
// color: Colors.purple,
decoration: BoxDecoration(
image: DecorationImage(
// image: AssetImage("lib/Images/knaus.jpg"),
image: AssetImage("lib/Images/innen.jpg"),
fit: BoxFit.cover,
),
),
child: !isReady
? Center(
child: Text(
"Warte auf Verbindung...",
style: TextStyle(fontSize: 24, color: Colors.red),
),
)
: Container(
child: StreamBuilder<List<int>>(
stream: wt_Ist_stream,
builder: (BuildContext context,
AsyncSnapshot<List<int>> snapshot) {
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
if (snapshot.connectionState ==
ConnectionState.active) {
_DataParser(snapshot.data);
The characteristics i generate with:
List<BluetoothService> services = await widget.device.discoverServices();
services.forEach((service) {
if (service.uuid.toString() == SERVICE_UUID) {
service.characteristics.forEach((characteristic) {
if (characteristic.uuid.toString() == CHARACTERISTIC_UUID_WT_RcvWater) {
characteristic.setNotifyValue(!characteristic.isNotifying);
characteristic1=characteristic;
// characteristic1.setNotifyValue(!characteristic.isNotifying);
wt_Ist_stream = characteristic1.value;
// wt_Soll_stream = service.characteristic.value;
setState(() {
isReady = true;
});
}
if (characteristic.uuid.toString() == CHARACTERISTIC_UUID_WT_Soll) {
characteristic.setNotifyValue(!characteristic.isNotifying);
characteristic2=characteristic;
// characteristic2.setNotifyValue(!characteristic2.isNotifying);
wt_Soll_stream = characteristic2.value;
// characteristic2.value.listen((InputString2)
// wt_Soll_stream = service.characteristic.value;
}
//Updating characteristic to perform write operation.
if (characteristic.uuid.toString() == CHARACTERISTIC_UUID_WT_TxWater) {
characteristic_Write=characteristic;
}
});
}
});
if (!isReady) {
_Pop();
}
}
Both reading-Characteristics are send Data to the app and i get the notification, that new data available [onCharacteristicChanged] uuid: 456e869c-d393-4cec-9f43-cef5382eab72].
but my Dataparser will only start if the value for uuid ...b72(wt_Ist_stream) changed. Then he gets the right string from snapshot.data.
If i changed the Streambuilder-Stream
child: StreamBuilder<List<int>>(
stream: wt_Ist_stream,
to
child: StreamBuilder<List<int>>(
stream: wt_Soll_stream,
my dataparser gets the value from characteristic 2. But how can i change my app that my parser starts automatic if one stream (wt_Ist_Stream or wt_Soll_stream) changed and then gets the right value.
The Streambuilder sends the right data to the dataparser but only the stream, which is called in stream:
How can i changed the code, that my parser starts on a stream-change and gets the right values from snapshot.data?
The dataparser-code:
_DataParser(List<int> dataFromDevice) {
String InputString = utf8.decode(dataFromDevice);
if (InputString.length>6) {.....}

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

How to apply an async operation on list view builder?

I am trying to get set of encrypted data documents from firebase and display them on a list view on flutter.
I used a stream builder for obtaining data and started displaying it on the list view. But I cant perform the decryption operation on each data item as it is an async operation. What is the best way to do this?
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection(ScopedModel.of<User>(context).userId)
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(
child: new Container(
child: CircularProgressIndicator(),
));
default:
if (snapshot.data.documents.length == 0) {
return Container(
padding: EdgeInsets.all(16.0),
child: Row(
children: <Widget>[
Text('Empty',),
],
),
);
}
final docs = snapshot.data.documents;
return ScrollConfiguration(
behavior: ScrollBehavior(),
child: ListView.builder(
itemCount: len,
scrollDirection: Axis.horizontal,
itemBuilder: (context, position) {
// Where should I decrypt the below data?
// let decrypted = await myDecryptionFunction(docs[position]['myDataKey']) ;
// the above is not working
// this will show the encrypted text
return Text(docs[position]['myDataKey']);
}
....
For your situation you can use a StreamController with a helper class to hold the information.
The following is just an example but adapt it to your own needs.
// Helper classes
// adapt it to your own business case
class Notes {
String title;
String description;
Notes({this.title, this.description});
}
class NotesFromDb {
String connectionState;
bool hasError;
String error;
List<Notes> notes;
NotesFromDb({this.connectionState, this.hasError, this.error, this.notes});
}
// The Streambuilder
StreamBuilder<NotesFromDb>(
stream: sController.stream,
builder: (BuildContext context, AsyncSnapshot<NotesFromDb> snapshot) {
// Here you can check for errors
if (snapshot.data.hasError == true) {
return Container(
color: Colors.red,
child: Text(snapshot.data.error),
);
}
// Here you can check for your connection state
if (snapshot.data.connectionState == 'Loading') {
return Container(
color: Colors.yellow,
child: CircularProgressIndicator(),
);
}
// Here you can show your data
var info = snapshot.data.notes.map((doc) {
return Text(doc.title);
});
return Center(
child: Container(
color: Colors.deepOrange,
child: Column(
children: info.toList(),
),
));
})
// Here is how you can handle the decrypt data
// using a FloatingActionButton for loading the data (for example)
FloatingActionButton(
onPressed: () async { // you would need to add the async
List<Notes> theNotes; //just to hold the information
// Use this to allow to show the CircularProgressIndicator
sController.sink.add(NotesFromDb(connectionState: 'Loading'));
var snapshots = Firestore.instance.collection('notes').snapshots();
snapshots.listen((QuerySnapshot data) {
theNotes = data.documents.map((DocumentSnapshot doc) {
// Build your data
return Notes(
title: doc.data['title'],
description: doc.data['description']);
}).toList();
}, onError: (err, stack) {
// If an error happend then send the error to the stream
sController.sink
.add(NotesFromDb(hasError: true, error: err.error));
});
// Here you can to decrypt the documents with your function
var decryptDocuments = await Future.delayed(Duration(seconds: 2)); //Whatever future function
// Once you have the decrypt documents, you would need to send that to the stream.
// set the connectionState to Done, so the spinner is not showed anymore.
sController.sink.add(NotesFromDb(
hasError: false, connectionState: 'Done', notes: decryptDocuments));
},
child: Icon(Icons.arrow_forward),
)
Take the example just for illustration on how to do it.
Hope this help.