StreamBuilder not refreshing after asyncMap future resolves - flutter

I'm using the following StreamBuilder in a Stateful widget:
StreamBuilder<List<int>>(
stream: widget.model.results(widget.type),
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
if (snapshot.hasError) return Text('Error');
final List<int> results = snapshot.data;
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
return _buildListTile(results[index]);
});
})
And here's the bit where the Streams get built:
// inside the ViewModel
late final List<StreamController> _streamControllers = [
StreamController<List<int>>.broadcast(),
StreamController<List<int>>.broadcast(),
];
List<int> _results = [];
Stream<List<int>> results(int index) =>
_streamControllers[index]
.stream
.debounce(Duration(milliseconds: 500))
.asyncMap((filter) async {
final List<int> assets = await search(filter); // 👈 Future
return _results..addAll(assets);
});
The issue is that the UI doesn't get rebuilt after the search results are returned.
The debugger shows that the Future is getting resolved correctly, but that the UI doesn't get rebuilt once the result is returned (within asyncMap).
Am I using asyncMap correctly? Is there an alternative way to set this up that could potentially get it working?
EDIT: Showing the code that adds events to the stream
[0, 1].forEach((index) =>
textController.addListener(() =>
_streamControllers[index]
.sink
.add(textController[index].text));

U are using asyncMap correctly.
Your issue might be that you add events to stream before Streambuilder starts to listen to widget.model.results(widget.type) stream.
Either use:
BehaviorSubject
final List<BehaviorSubject> _streamControllers = [
BehaviorSubject<List<int>>(),
BehaviorSubject<List<int>>(),
];
or add events AFTER widgets are built (or when we start to listen to them)
How to use onListen callback to start producing events?

You are creating a new Stream every build therefore it will be always empty and won't update correctly. You have the same controller, but asyncMap is creating a new Stream under the hood. The docs:
Creates a new stream with each data event of this stream asynchronously mapped to a new event.
The fix would be to save the instance of the stream after asyncMap is used. This can be done multiple ways. One would be to make a late initialized field inside your State.
late Stream<List<int>> myStream = widget.model.results(widget.type);
and then use this instance in the StreamBuilder:
StreamBuilder<List<int>>(
stream: myStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
if (snapshot.hasError) return Text('Error');
final List<int> results = snapshot.data;
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
return _buildListTile(results[index]);
});
})
But you can also save the instance in initState or completely outside the widget and make Stream<List<int>> results(int index) return the saved instance or make it the list like this:
List<Stream<List<int>>> results = _streamControllers
.map((s) => s.stream.asyncMap((filter) async {
final List<int> assets = await search(); // 👈 Future
return _results..addAll(assets);
}))
.toList();

Related

Snapshot data null but FutureBuilder return data?

I have http response data but IT IS NULL?????
...
Future getcategoryimage() async{
var url = "http://172.17.40.225/shoplacviet/getcategoryimage.php";
var reponse = await http.get(Uri.parse(url));
var list = reponse.body;
Uint8List _bytesImage;
_bytesImage = Base64Decoder().convert(list);
return _bytesImage;
}
...
FutureBuilder(
future: getcategoryimage(),
builder: (context,snapshot){
List lista = snapshot.data as List;//------------> I have http response data but IT IS NULL?????
if(snapshot.hasError) print(snapshot.error);
return snapshot.hasData ? ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: lista.length,
itemBuilder: (context,index){
var blob = lista[index]['categoryimage'];
Uint8List _bytesImage;
_bytesImage = Base64Decoder().convert(blob);
return Container(
child: Image.memory(_bytesImage),
);
}):Center(child: CircularProgressIndicator(),) ;
},
),
Do not access data before it is available. Use hasData and hasError properties something like this:
FutureBuilder<future type>(
future: _future, // a previously-obtained Future
builder: (BuildContext context, AsyncSnapshot<future type> snapshot) {
if (snapshot.hasData) {
// here snapshot.data is available
return <hasData widget>
} else if (snapshot.hasError) {
return <hasError widget>
} else {
return <waiting widget>
}
}
)
You're building the future as the future: argument to your FutureBuilder. Since this is in a build() method, your future might be getting reset up to 60 times per second. The proper strategy (according to the first few paragraphs of the documentation) is:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
A general guideline is to assume that every build method could get called every frame, and to treat omitted calls as an optimization.
So there you have it. Move your call to getcategoryimage() out into initState(), saving it into a State variable.
I illustrate this in a 10-minute video, if you need further clarification.

How can i fetch data from Firestore (the cashed data) in flutter

i am trying to save data reads which have been not changed yet to avoid more and more the same repeated data that not changed yet ..
i have normal Future.Builder that get data from firstore (network side)
Widget build(BuildContext context) {
return FutureBuilder(
future: FirebaseFirestore.instance.collection('users').get(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
return const Expanded(child: SizedBox()) ;
}
return ListView.builder(
itemCount: snapshot.data!.docs.length ,
itemBuilder: (context, int index) {
DocumentSnapshot documentSnapshot = snapshot.data!.docs[index];
return ListView.builder(
itemCount: snapshot.data!.docs.length ,
itemBuilder: (context, int index) {
DocumentSnapshot documentSnapshot = snapshot.data!.docs[index];
return Text(documentSnapshot['products'])
}
);
}
}
and i have into every single document Timestamp and i need to use where('modify',isGreaterThen : HERE i need to put the old timestamp from cashe to chick if it not changed yet to decide to fetch the new ones
in flutter i cannot handle it as well .. How can i fetch the cashed data with the new ones from network in the harmonic index such as reading the whole data in normal way .. so i avoided these old ones to be reload again ..
i have read a lot of this topic but it was in old Firestore version also it was using java code ...
this following code that cannot handle in flutter
Source CACHE = Source.CACHE;
Source SERVER = Source.SERVER;
Query.Direction DESCENDING = Query.Direction.DESCENDING;
FirebaseFirestore rootRef = FirebaseFirestore.getInstance();
CollectionReference shoesRef = rootRef.collection("shoes");
Query lastAddedQuery = shoesRef.orderBy("lastModified", DESCENDING)
shoesRef.get(CACHE).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
boolean isEmpty = task.getResult().isEmpty();
if (isEmpty) {
shoesRef.get(SERVER).addOnCompleteListener(/* ... */);
}
}
});
Query query = shoesRef.orderBy("lastModified", DESCENDING)
.whereGreaterThan("lastModified", savedDate);
source code was written by Alex Mamo
https://medium.com/firebase-tips-tricks/how-to-drastically-reduce-the-number-of-reads-when-no-documents-are-changed-in-firestore-8760e2f25e9e
any support or example with latest version of Firbase and in dart or flutter code will be so thankful ..
best regards

Flutter Firestore Convert Stream To Future

I have a firestore stream in flutter that I would instead like to be a future so that I can do pagination of requests. Currently I periodically increase the limit variable in the code below and reload the whole original stream plus new data. This is very irritating because every time the limit variable increases the widget (a listview) scrolls to the top. I would like to ask how to convert the stream below into a future and how to place its contents into a list. My purpose of doing so being that the contents of all the future calls will be accumulated in an list and my listview will be generated off of that array, hopefully without scrolling to the top every time.
My other reason for doing so is to save memory on the client device. When a user scrolls down I would like to remove items from the front of the list to save memory and reload them only if the user scrolls back up. My project is a social-media application so I foresee users scrolling down indefinitely and using up all their phone memory. I am new to flutter so I would also like to ask if this memory usage is a valid concern.
Stream<List<Memo>> getFeed(int limit) async* {
yield* Firestore.instance
.collection('memos')
.where('followers', arrayContains: userid)
.orderBy("date")
.limit(limit) // TODO: add pagination of request
// .startAfterDocument(null)
.snapshots()
.map(_snapshotToMemoList);
}
My Streamsubscription and listview builder code is as follows:
Widget build(BuildContext context) {
return StreamBuilder<List<Memo>>(
stream: dbService( user: widget.user ).getFeed( streamLimit ),
builder: (BuildContext context, AsyncSnapshot<List<Memo>> snapshot) {
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading...');
default:
if (snapshot.data.isEmpty) {
return Text('EMPTY');
}
// streamSub.cancel();
// return Text(snapshot.data[1].body);
return ListView.builder(
controller: _scrollController,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 600,
child: Text(snapshot.data[index].body)
);
}
);
}
},
);
Lastly, my limit increasing function is
void initState() {
if (lastScrollPosition != null) _scrollController.jumpTo(lastScrollPosition);
_scrollController.addListener(() {
final maxScroll = _scrollController.position.maxScrollExtent;
// print(maxScroll);
final currentScroll = _scrollController.position.pixels;
// print(currentScroll);
if (maxScroll - currentScroll <= _scrollThreshold) {
setState(() {
lastScrollPosition = currentScroll;
streamLimit += 1;
print('increasing');
});
}
});
}

Flutter Streambuilder map to List object

I need to display a listview in Flutter with data from firestore. Then I want the user to be able to filter the listview by typing his query in a textfield in the appbar. This is the code I came up with for the listview:
_buildAllAds() {
return StreamBuilder(
stream: Firestore.instance.collection("Classificados")
.orderBy('title').snapshots().map((snap) async {
allAds.clear();
snap.documents.forEach((d) {
allAds.add(ClassificadoData(d.documentID,
d.data["title"], d.data["description"], d.data["price"], d.data["images"] ));
});
}),
builder: (context, snapshot) {
// if (!snapshot.hasData) {
// return Center(child: CircularProgressIndicator());
// }
//else{
//}
if (snapshot.hasError) {
print("err:${snapshot.error}");
}
return ListView.builder(
itemCount: allAds.length,
itemBuilder: (context, index) {
ClassificadoData ad = allAds[index];
return ClassificadosTile(ad);
});
});
}
The reason I save the stream data in the List allAds of type ClassificadoData (data items are ads) is because I can then copy it to another List filteredAds on which the user can perform filtering. And the reason I need a stream for allAds is because I want users to be able to see additions/updates in real time.
So this code "works" but it feels a bit awkward and I also can't do nothing with the builder since snaphot remains null all the way (can't show loader during initial data fetch, for example).
Was wondering if there's maybe a more solid way for doing what I want and if it's possible to get a reference to the snapshots down to the builder.
You seem to be mixing two different concepts of using Streams and Stream related Widgets. Ideally you would either use a StreamBuilder and use the data you get from the stream directly on the Widget, or listen to the data and update a variable that is then used to populate your ListView. I've build the latter as an example from your code:
#override
initState(){
_listenToData();
super.initState();
}
_listenToData(){
Firestore.instance.collection("Classificados")
.orderBy('title').snapshots().listen((snap){
allAds.clear();
setState(() {
snap.documents.forEach((d) {
allAds.add(ClassificadoData(d.documentID,
d.data["title"], d.data["description"], d.data["price"], d.data["images"] ));
});
});
});
}
_buildAllAds() {
return ListView.builder(
itemCount: allAds.length,
itemBuilder: (context, index) {
ClassificadoData ad = allAds[index];
return ClassificadosTile(ad);
}
);
}

Itembuilder doesn't execute in streambuilder, when updating data in firestore

When I launch the app for the first time it loads in all the items into the animated list. But when If I add an item to firestore, the streambuilder realizes something has happend. BUT it doesn't execute the itembuilder so no new items will show up. However if I relaunches the app, the new items will be loaded. If I would just change a name on an existing item on the firestore database, it will rebuild with no problem.
When I update an item. I have to get the map of tasks and replace it with a new copy that has the item and post/update that to firestore.
I have tried everything I can think off, doesn't get why this happens.
I have printed in the itembuilder and can see that it doesn't execute on an update.
Widget _buildTasksList() {
return new Expanded(
child: new StreamBuilder(
stream: Firestore.instance
.collection("lists")
.document(tasklist.postID)
.snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return new Text("Loading");
}
var listdata = snapshot.data;
tasklist.tasks = listdata['tasks'];
tasklist.owners = listdata['owners'];
tasklist.sorter = listdata['sorter'];
return new AnimatedList(
initialItemCount: convertToTaskList(listdata['tasks']).length,
key: _listKey,
itemBuilder: (context, index, animation) {
return new TaskRow(
task: this.listModel[index],
animation: animation,
listModel: this.listModel,
tasklist: tasklist,
onChange: () => _onChange(this.listModel[index]),
);
},
);
},
)
);
}
Got it working by resetting my _listKey, added this before animated list.
_listKey = null;
_listKey = new GlobalKey();