Nested Futures for FutureBuilder - firebase

I have a FutureBuilder widget that should wait for data from a firestore collection.
class MyScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: calendarQuery.getCalendarEntry(dateString),
builder: (BuildContext context, AsyncSnapshot snap) {
if (snap.hasData) {
List<Events> recipe = snap.data;
return Scaffold(
appBar: AppBar(
title: Text("Events"),
),
body: Column(
children: <Widget>[
...,
],
),
);
} else {
return LoadingScreen();
}
},
);
}
}
I retrieve a list of events and then for each event I need to fetch some additional details. I tried to do nested Futures and came up with the code below. It generates a Future<Iterable<Future<Detail>>> which ends up as MappedListIterable<DocumentSnapshot,Future<Recipe>> in snap.data and i cannot handle it.
class CalendarQuery<T> {
...
Future<Iterable<Future<Detail>>> getCalendarEntry(String date, String type) async {
return await ref
.where("date", isEqualTo: date)
.getDocuments()
.then((data) {
return data.documents.map((doc) => Document<Details>(path: 'detailCollection/${doc.data["Event"]["SomeId"]}')
.getData());
});
}
}
I think I went wrong here at some points with handling the futures and there is probably a proper way to do this.
Does someone know a way to refactor getCalendarEntry so that it returns a Future<List<T>>? Or maybe there is a better approach to solve this?

You can use Future.wait to create a Future that completes once all Futures from an Iterable have completed.
In your scenario, you would replace return await ref with return Future.wait(ref and a closing bracket where needed, to create a Future that waits for all Details to be retrieved.

Related

Flutter Firestore: FirestoreBuilder with initial data

I'm making my first Flutter app and I encounter a problem and doesn't found any solution for it.
I have a view where I render a Firestore document, and there is two ways of getting there:
From a list where I already loaded my documents
From Dynamic Links with uid attached as arguments (args)
So in order to listen document changes and loading the data when arriving from the link I used FirestoreBuilder like this:
return FirestoreBuilder<EventDocumentSnapshot>(
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<EventDocumentSnapshot> snapshot, Widget? child) {
if (!snapshot.hasData) {
return Container();
}
Event? event = snapshot.requireData.data;
return Scafold(); //Rest of my rendering code
}
);
How I could avoid first call to Firebase when I already have the data but still listen to changes? The main problem is that my hero animation doesn't work because of this.
I tried with a StreamBuilder and initialDataparam but since it's expecting stream I didn't know how to cast my data.
Okay, so I found the solution myself after many tries, so I added my Model object that can be null as initialData, but the thing that makes me struggle with is how you get the data in the builder. You have to call different methods depending on where the data is coming from.
return StreamBuilder(
initialData: args.event
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
// Here is the trick, when data is coming from initialData you only
// need to call requireData to get your Model
Event event = snapshot.requireData is EventDocumentSnapshot ? snapshot.requireData.data : snapshot.requireData;
return Scafold(); //Rest of my rendering code
}
);
Reading through cloud_firestore's documentation you can see that a Stream from a Query can be obtained via snapshots()
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document['title']),
subtitle: new Text(document['author']),
);
}).toList(),
);
},
);
This won't help you, but with GetX it's simple to implement like this: You don't need StreamBuilder anymore.
//GetXcontroller
class pageController extends GetXcontroller {
...
RxList<EventModel> events = RxList<EventModel>([]);
Stream<List<EventModel>> eventStream(User? firebaseUser) =>
FirebaseFirestore.instance
.collection('events')
.snapshots()
.map((query) =>
query.docs.map((item) => UserModel.fromMap(item)).toList());
#override
void onReady() async {
super.onReady();
events.bindStream(
eventStream(controller.firebaseUser)); // subscribe any change of events collection
}
#override
onClose() {
super.onClose();
events.close(); //close rxObject to stop stream
}
...
}
You can use document snapshots on StreamBuilder.stream. You might want to abstract the call to firebase and map it to an entity you defined.
MyEntity fromSnapshot(DocumentSnapshot<Map<String, dynamic>> snap) {
final data = snap.data()!;
return MyEntity (
id: snap.id,
name: data['name'],
);
}
Stream<MyEntity> streamEntity(String uid) {
return firebaseCollection
.doc(uid)
.snapshots()
.map((snapshot) => fromSnapshot(snapshot));
}
return StreamBuilder<MyEntity>(
// you can use firebaseCollection.doc(uid).snapshots() directly
stream: streamEntity(args.uid),
builder: (context, snapshot) {
if (snapshot.hasData) {
// do something with snapshot.data
return Scaffold(...);
} else {
// e.g. return progress indicator if there is no data
return Center(child: CircularProgressIndicator());
}
},
);
For more complex data models you might want to look at simple state management or patterns such as BLoC.

future.then block skipped inside function of return type integer causes to return null

I have stumbled upon a weird problem.
The following function gets a Firestore document and returns it so other functions can access it's data
Future getCountRequests() async {
try{
return await _countReference.document('Requests').get();
} catch(e){
print(e.toString());
}
}
And this is the function in question that uses it's data.
int _countRequest() {
int toReturn;
CounterService().getCountRequests().then(
(doc) {
//print('Item in question: $doc, information from document ${doc.data}');
//this line prints correctly Instance of DocumentReference and {"amount" : 6}
toReturn = doc.data['amount'];
}
);
return toReturn;
}
When I run the code, I get an error message on my screen which states that the AnimatedList I am using receives null from the _countRequest() function.
Putting a break on this line has helped me understand that this block gets skipped completely
CounterService().getCountRequests().then( ...
However when I put a break on this line, it shows that the code inside the block works and that the document is indeed received through the getCountRequests() function.
toReturn = doc.data['amount'];
My question is, what causes the .then block to be skipped causing the function to return null?
What you are trying to do cannot work.
You are trying to return the result of an asynchronous computation from a synchronous function. There is no way to make that work.
An asynchronous computation will always complete later, and a synchronous function always returns now, so the result of the computation cannot be returned now.
The then call is what makes the operation asynchronous. It passes a callback to a future, but the only thing you are guaranteed about when that callback is called is that it is definitely not immediately. The then call returns immediately, returning a future which will be completed when the callback has been called, and your code returns the current value of toReturn, which is still null.
In the end I figured that doing this asynchronously with a FutureBuilder wouldn't work at all, or with my current skillset I just can't figure out how to make it work. So I decided to convert to a StreamBuilder which uses a Stream<QuerySnapshot> gotten from the following function.
Stream<QuerySnapshot> findAllRequests() {
try{
return accountRequestCollection.snapshots();
} catch(e){
print(e.toString());
return null;
}
}
and built the AnimatedList into the StreamBuilder like so.
class Request extends StatefulWidget {
#override
_RequestState createState() => _RequestState();
}
class _RequestState extends State<Request> {
final AdministrationService _administrationService = AdministrationService();
final GlobalKey<AnimatedListState> _globalKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Account Requests'),
),
body: StreamBuilder(
stream: _administrationService.findAllRequests(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Loading();
default: if (snapshot.hasError) {
return new Text('Error: ${snapshot.hasError}');
} else {
return AnimatedList(
key: _globalKey,
initialItemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index, Animation animation) {
return _buildItem(snapshot.data.documents[index].data['email'], animation);
}
);
}
}
}
)
);
}
Widget _buildItem(String item, Animation animation) {
return SizeTransition(
sizeFactor: animation,
child: Card(
child: ListTile(
title: Text(
item,
style: TextStyle(
fontSize: 20.0,
)
),
),
),
);
}
}
It results into this page.
Emulator Screenshot.
I hope that this solution can help others with similar problems, though I am not sure how costly this could get when it comes to firestore read/writes considering this is just a proof of concept product.

Firestore collection query as stream in flutter

I'm trying to query a few documents from a collection, this query should listen to changes made in the queried documents, so I'd need a stream. I'm doing following (in Dart/Flutter)
Stream<List<MatchRequest>> _getNewMatches() {
return Collection<MatchRequest>(path: 'requests')
.ref
.where('status', isNull: true)
.where('users', arrayContains: ['$currentUid'])
.orderBy('last_activity')
.snapshots()
.map((list) => list.documents.map(
(doc) => Global.models[MatchRequest](doc.data) as MatchRequest));
}
(The object Collection sets the path to the ref in it's constructor, eg: ref = db.collection($path) and the map makes a model of the results)
Then I'm using a StreamBuilder with stream invoking the method above and builder checking if snapshot.hasData. But it keeps loading, snapshot.hasData keeps being false. What am I doing wrong here?
EDIT:
My firestore security rules contain:
match /requests/{requestId} {
allow read: if isLoggedIn();
allow write: if isLoggedIn();
}
When removing every where and orderBy, it doesn't find anything as well. And there are documents present in the requests-collection
When trying to query only 1 document as a stream from the requests-collection, he does find the result
Is it because I should add indexes to my firestore indexes? But this won't solve my first problem which is that even without where and orderBy, it doesn't get any data
I've written a simple example of it seems to be like what you are trying to do but are missing the listen() method:
Firestore.instance.collection('collection')
.where('field', isEqualTo: 'value')
.orderBy('field')
.snapshots()
.listen((QuerySnapshot querySnapshot){
querySnapshot.documents.forEach((document) => print(document));
}
);
This is just an example of how you can take the data from a Firestore Stream and use it on a StreamBuilder:
class _MyHomePageState extends State<MyHomePage> {
Stream dataList;
#override
void initState() {
dataList = Firestore.instance.collection('collection')
.where('field', isEqualTo: 'value')
.orderBy('field')
.snapshots();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: StreamBuilder(
stream: dataList,
builder: (context, asyncSnapshot) {
if(asyncSnapshot.hasError)
return Text('Error: ${asyncSnapshot.error}');
switch (asyncSnapshot.connectionState) {
case ConnectionState.none: return Text('No data');
case ConnectionState.waiting: return Text('Awaiting...');
case ConnectionState.active:
return ListView(
children: asyncSnapshot.data.map((document) => Text(document['value'])),
);
break;
case ConnectionState.done: return ListView(
children: asyncSnapshot.data.map((document) => Text(document['value'])),
);
break;
}
return null;
}),
),
);
}
}

Flutter How to pass in documents.length from Firestore in another method?

I have a method which takes an int value, which is supposed to be the value of the length of an array field in a document in a collection in Cloud Firestore.
I'm very new to both Flutter and especially Firestore, and I figure it has something to do with how futures, queries and streams works, but I can't seem to understand it correctly.
This is what I'm trying to achieve:
class UserBookmarksSection extends StatefulWidget {
#override
_UserBookmarksSectionState createState() => _UserBookmarksSectionState();
}
class _UserBookmarksSectionState extends State<UserBookmarksSection> {
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
HeadlineItem(title: "Your bookmarks"),
StreamBuilder(
stream: Firestore.instance.collection("brites").snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text("Loading...");
return Column(
children: populateBriteList(snapshot, x, true), //Here I want to pass in the length of the array in firestore into x
);
}
),
],
);
}
}
Basically I've tried to create a method which returns a Future<int> and tried to pass that, but that didn't work because a Future<int> isn't the same type as int. That I understand, but the solution is not clear to me.
I'm not sure the relationship between brites and bookmarks- that is, which you expect to change more often. I'm going to assume that the bookmarks isn't changing once the user opens the bookmarks section.
If that is the case you can retrieve the bookmarks length value in initState. You will override the state class's initState() method, and then call an async method which retrieves the value from the database. (init state can't be async itself). Once the value is retrieved, you can then call setState() to update the widget with the value of bookmarks set as an int.
Might look something like this:
class UserBookmarksSection extends StatefulWidget {
#override
_UserBookmarksSectionState createState() => _UserBookmarksSectionState();
}
class _UserBookmarksSectionState extends State<UserBookmarksSection> {
int bookmarksLength;
#override
void initState(){
super.initState();
getBookmarksLength();
}
Future<void> getBookmarksLength() async {
bookmarksLength = await getArrayLength(id);
setState((){});
}
#override
Widget build(BuildContext context) {
if(bookmarksLength == null) return CircularProgressIndicator();
else return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
HeadlineItem(title: "Your bookmarks"),
StreamBuilder(
stream: Firestore.instance.collection("brites").snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text("Loading...");
return Column(
children: populateBriteList(snapshot, bookmarksLength, true),
);
}
),
],
);
}
}
Firestore firestore = Firestore.instance;
Future<int> getArrayLength(String documentId) async {
DocumentSnapshot snapshot = await firestore.collection('collection').document(documentId).get();
return snapshot.data['array'].length;
}
A more robust solution might be to have a separate class that handles both listening to the stream and retrieving the length of the array, and then combining all of the information you're going to display into some abstract data type which you put into a separate stream you specify that is specifically for the UI to listen to.

Get request is not done with Flutter

I'm doing an Flutter application, but I found a problem when I wanted to make some http request. The problem is that when I want to make it, I define the function as asynchronous, and I write await before calling the function http.get(), but the function it´s not executed and the code after the function is not executed also.
The code is below and no error is thrown.
class db{
void get_basic() async{
String url = 'http://example.org/';
Response response = await get(url);
int statusCode = response.statusCode;
print("Listo");
print(statusCode);
}
}
Widget build(BuildContext context){
print("inicio database");
db database = db();
database.get_basic();
print("final database");
main_content main = main_content();
return Scaffold(
appBar: AppBar(
title: Text('Title),
),
body: main,
bottomNavigationBar: bottomNavBar(0,main.refresh),
);
}
async functions always return a future.
A Future object represents a computation whose return value might not yet be available. The Future returns the value of the computation when it completes at some time in the future. Futures are often used for potentially lengthy computations such as I/O and interaction with users.
async functions return futures, that means that they are performed in the future.
because dart uses a single thread to run code that means when it hits await instead of blocking the thread it will move to after the function call and starts exciting code again until it idles.
when the thread has done all of the synchronous code it will go back to await
line and start exciting there.
The get_basic() code should be called after the build is complete.
if you want to rebuild your widget after get_basic() is completed you need to use a FutureBuilder:
class _MyHomePageState extends State<MyHomePage> {
db database;
#override
void initState() {
super.initState();
database = db();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
),
body: FutureBuilder(
future: database.get_basic(),
builder: (context,snapshot){
if(snapshot.connectionState!=ConnectionState.done){
return Center(
child: Text('Loading...'),
);
} else {
if(snapshot.hasError){
return Center(
child: Text(snapshot.error.toString()),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index){
return ListTile(title: Text(snapshot.data[index]['title']),);
},
);
}
},
),
);
}
}
class db{
Future get_basic() async{
try {
String url = 'https://jsonplaceholder.typicode.com/posts';
Response response = await get(url);
return jsonDecode(response.body);
} catch (e) {
print(e);
return [{}];
}
}
}

Resources