Flutter Firestore Convert Stream To Future - flutter

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');
});
}
});
}

Related

How to dynamically update ListView while fetching data asynchronously?

I am trying to figure out the best way to update a ListView.builder() while I'm fetching a list of data. Essentially, I am downloading data in batches -- let's say a group of 10 images at a time -- and displaying them in a ListView.builder after the future completes, with an indicator below it to signify that we're still fetching data. And do this until everything is fetched.
What's the best way of going about this?
Example code of what I have:
void _fetchImages() async {
// Fetch images
for (...) {
final results = await Future.wait[imageFutures];
// update list here
imageList.addAll(results); // let's say data comes back in correct format
setState((){});
}
}
#override
void initState() {
super.initState();
_fetchImages();
}
#override
Widget build(BuildContext context) {
return ListView.builder(...);
}
Return List from Stream then you can use StreamBuilder.
return StreamBuilder<List<MyImage>>(
stream: dataStream,
builder: (BuildContext context, AsyncSnapshot<List<MyImage>> snapshot) {
if (snapshot.hasData) {
return Center(
child: ListView.builder(
...
);
}
return SomeWidget(
...
);
},
);

Flutter FutureBuilder does not stop showing CircularProgressIndicator

I am trying to receive data using a FutureBuilder, but it hangs on the CircularProgressIndicator. I think it's remaining on ConnectionState.waiting but I'm not sure why.
#override
initState() {
_widgetList = getWidgetList();
}
Stream<List<String>> getFriendUUIDS() => Firestore.friends
.doc(gameManager.myself.uuid)
.snapshots()
.map((snapshot) => ((snapshot.data()?.keys)?.toList()) ?? []);
Future<List<MomentWidget>> getWidgetList() async{
List<MomentWidget> widgetList = [];
Set<String> momentIds = Set();
await for (final uuids in getFriendUUIDS()){
for (String uuid in uuids){
DocumentSnapshot<Map<String, dynamic>> values = await Firestore.users
.doc(uuid)
.get();
for (String momentId in values.data()?['moments'] ?? [] ){
momentIds.add(momentId);
}
}
}
for (String momentId in momentIds){
DocumentSnapshot<Map<String, dynamic>> values =
await Firestore.instance.collection('moments').doc(momentId).get();
Map<String, dynamic>? data = values.data()!;
String downloadURL = await storage.ref('moments/$momentId').getDownloadURL();
MomentWidget widget = MomentWidget(numLikes: data['liked_by'].length ,
location: data['location'],
date: data['timestamp'],
names: data['taggedFriends'].toString(),
shotBy: data['taken_by'], image: NetworkImage(downloadURL));
widgetList.add(widget);
}
return widgetList;
}
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container(
height: size.height,
width: size.width,
child: FutureBuilder(
future: _widgetList,
builder: (context, AsyncSnapshot<List<MomentWidget>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemBuilder: (context, pos) {
return snapshot.data![pos];
},
);
}
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
default:
return Text('Unhandled State');
}
}
),
);
}
I have tried to get the Future inside of initState(), and have tried to use snapshot.hasData instead, to no avail.
I have encountered a similar problem. When building an object from json , if the types don't match , it can quietly fail. I do not think your widgetList is ever returned. In my case I had a variable "cost" that I thought would be of type int , however in the database it was of type String. It always quietly failed and never showed the markers on the map widget
So:
Check how many times that loop of yours is executed. Probably only once and then it quietly fails
If the above happens:
Makes sure the types of your variables match the ones from the database. Comment out every variable one by one to find where the problem is.
Let me know if it works

Flutter: StreamBuilder with Firebase

I have a ListView of objects from Firebase in which I would like to have it refresh using a StreamBuilder when the data changes.
I can load up my list fine & when data changes my list does refresh.
The issue I am having is instead of the ListTile that has the change just updating, I see that tile being duplicated so I see the new change & the old change.
Here's my setup:
final ref = FirebaseDatabase.instance.reference();
late DatabaseReference itemRef;
late FirebaseDatabase database = FirebaseDatabase();
late StreamSubscription _objectInfoStreamSub; // Not sure if needed?
late List<CustomObject> data = [];
#override
void initState() {
super.initState();
final keys = Global.kData.keys;
for (final key in keys) {
// Initialize this...
itemRef = database.reference().child('ABC').child(key.toString());
}
// Load the data...
_setListeners();
}
// Here is where I load my data initially...
Future<void> _setListeners() async {
// Clear out our data before we reload...
data.clear();
final keys = Global.kData.keys;
for (final key in keys) {
_objectInfoStreamSub =
ref.child("ABC").child(key.toString()).onValue.listen(
(event) {
setState(() {
// Mapped the data...
final firebaseData = Map<String, dynamic>.from(event.snapshot.value);
// Create the Room...
final room = CustomObject.fromJson(firebaseData);
// Check if blocked...
// Logic to see if user is blocked
// check if isArchived
// Logic to see if room is archvied
if (!isBlocked && !isArchived) {
if (!data.contains(room)) {
data.add(room);
}
}
// Sort by date
// Logic to sort so the most recent is at top
});
},
);
}
}
// Here is my updated StreamBuilder...
SliverToBoxAdapter(
child: StreamBuilder<dynamic>(
stream: itemRef.child('ABC').onValue,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.active) {
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
return ChatRoom(
data: data[index],
);
},
);
} else {
return Container();
}
},
),
),
Not sure if this causes your problem, but try wrapping the ListView.builder() with StreamBuilder instead of only it's items. Because in your current state of code, if you would add another item and your data.length would change, the ListView.builder() wouldn't get rebuilt and it wouldn't build new data.

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);
}
);
}

How to make the connection to waiting state by using StreamBuilder in flutter

My requirement is to make that StreamBuilder connection state to waiting.
I'm using publish subject, whenever I want to load data in stream builder I'm just adding data to the sink by calling postStudentsToAssign() method, here this method making an API call which takes some time, in that time I to want make that streamBuilder connection state to waiting
Stream Builder:
StreamBuilder(
stream: studentsBloc.studentsToAssign,
// initialData: [],
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
// While waiting for the data to load, show a loading spinner.
return getLoader();
default:
if (snapshot.hasError)
return Center(child: Text('Error: ${snapshot.error}'));
else
return _getDrawer(snapshot.data);
}
}),
Initializing Observable:
final _assignStudentSetter = PublishSubject<dynamic>();
Observable<List<AssignMilestoneModel>> get studentsToAssign =>
_studentsToAssignFetcher.stream;
Method that add's data to Stream:
postStudentsToAssign(int studyingClass, String milestoneId, String subject,
List studentList) async {
var response = await provider.postAssignedStudents(
studyingClass, milestoneId, subject, studentList);
_assignStudentSetter.sink.add(response);
}
You can send null to the stream, so the snapshot.connectionState changes to active. I don't know why and whether it's official solution, but it works (at least now). I found this accidentally.
I would like the Flutter team to explain how to set snapshot's connectionState. It's not clear from StreamBuilder documentation. It seems you should replace the stream with a new one to have snapshot in waiting state. But it's agains the logic you want to implement.
I checked StreamBuilder source to find out that the AsyncSnapshot.connectionState starts as waiting (after stream is connected), after receiving data changes to active. snapshot.hasData returns true if snapshot.data != null. That's how following code works.
class SearchScreen extends StatelessWidget {
final StreamController<SearchResult> _searchStreamController = StreamController<SearchResult>();
final SearchService _service = SearchService();
void _doSearch(String text) async {
if (text?.isNotEmpty ?? false) {
_searchStreamController.add(null);
_searchService.search(text)
.then((SearchResult result) => _searchStreamController.add(result))
.catchError((e) => _searchStreamController.addError(e));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: <Widget>[
SearchBar(
onChanged: (text) => _doSearch(text),
),
StreamBuilder<SearchResult>(
stream: _searchStreamController.stream,
builder: (BuildContext context, AsyncSnapshot<SearchResult> snapshot) {
Widget widget;
if (snapshot.hasData) {
widget = Expanded(
// show search result
);
}
else if (snapshot.hasError) {
widget = Expanded(
// show error
);
}
else if(snapshot.connectionState == ConnectionState.active){
widget = Expanded(
// show loading
);
}
else {
// empty
widget = Container();
}
return widget;
},
),
]),
);
}
}