How to reference Firestore documentID in a Flutter FutureBuilder - flutter

I have working code that gets a collection ('songs') from Firestore using a Future and a QuerySnapshot. I have that in a small function getSongs(). While I'm inside that function I have access to the documents' IDs ... so if I call say:
print(songsQuery.documents[1].documentID);
I get -LSvpZxM2pUIYjjp0qby
But later in my code I use a FutureBuilder where I call getSongs() for the future: and then build out a ListView with tiles of song info (Artist, Title, etc) from the snapshot in the builder:.
While I'm now in this widget I can't seem to figure out how to reference my .documentID anymore. I can get to all the .data elements for each document...but not the actual documentID.
Is there something very obvious that I'm missing?
Thanks for any help.
ER
I have scoured the internet trying to resolve with no luck. It seems like many people take the list of documents, load them into an array, add the doc.id, push it all into an array of items. Then use items. I would like to just use the snapshots as rendered back from Firestore and reference the doc.id directly if possible.
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
class AllSongs extends StatelessWidget {
Future getSongs() async {
var firestore = Firestore.instance;
QuerySnapshot songsQuery = await firestore.collection('songs').getDocuments();
print(songsQuery.documents[1].documentID);
//Here I can get to documentID...
return songsQuery.documents;
}
#override
Widget build(BuildContext context) {
return Center(
child: FutureBuilder(
future: getSongs(),
builder: (_, songSnapshots){
print('How do I reference the DocumentID in here?');
print(songSnapshots.data.length);
print(songSnapshots.data[0].data['title']);
//print(songSnapshots.data[0].documentID);
//print(songSnapshots.data[0].ref);
//print(songSnapshots.data[0].data[ DOCUMENTID?? ]);
if(songSnapshots.connectionState == ConnectionState.waiting){
return Center(
child: Text('Loading...'),
);
} else {
return ListView.builder(
itemCount: songSnapshots.data.length ,
itemBuilder: (_, index){
return ListTile(
title: Text(songSnapshots.data[index].data['title']),
subtitle: Text(songSnapshots.data[index].data['artist']),
);
});
}
},
)
);
}
}

You're trying to access the value the value of the future before it resolves.
Try adding this line:
if (!songSnapshots.hasData) {
// Future hasn't resolved
return something;
}
// Future has resolved, you can access your data (including documentId)
Your future resolves to a list of DocumentSnapshot so just wait for the future to resolve and you should have access to all your data. Alternatively, you can try to access this inside your else statement where the state of the connection is not waiting, but in this case you are considering any non-waiting states as successful so I'd recommend using the hasData property of the AsyncSnapshot class instead.

Related

What would be the most effective work around/ bypass for the Wherein 10 limit for flutter firebase? [duplicate]

This question already has answers here:
Is there a workaround for the Firebase Query "IN" Limit to 10?
(10 answers)
Firebase Firestore query an array of more than 10 elements
(5 answers)
Closed 19 days ago.
I am currently working on a screen in a project where the user can see a list of posts that he or she has subscribed, ordered by the time they were last updated (using a timestamp field in the post document). Currently this is done by retrieving an array of Post Document IDs from the user's profile document through a future builder, and then using that array for a wherein query in a streambuilder that works with a listview builder so they can do the lazy loading thing and also can update in real time. My problem is the wherein limit of 10 values in the array, and I'm not certain of the way I should go about working around it. This is my code:
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:testApp/myServices/postDisplay.dart';
class homeView extends StatefulWidget {
const homeView({Key? key}) : super(key: key);
#override
State<homeView> createState() => _homeViewState();
}
class _homeViewState extends State<homeView> {
final FirebaseAuth auth = FirebaseAuth.instance;
final db = FirebaseFirestore.instance;
late String currentUID;
bool isReady = false;
List subscriptions = ['1, 2, 3'];
Stream<QuerySnapshot<Object?>>? postStream;
getProfileData() async {
return await db.collection("Profiles").doc(currentUID).get().then((value) {
subscriptions = value.data()!["subscriptions"];
postStream = db
.collection("Posts")
.where(FieldPath.documentId, whereIn: subscriptions)
.snapshots();
isReady = true;
});
}
void initState() {
currentUID = auth.currentUser!.uid;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: getProfileData(),
builder: (context, snapshot) {
if (isReady) {
return StreamBuilder<QuerySnapshot>(
stream: postStream,
builder: ((context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return Center(
child: Container(
width: 300,
child: ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
return postDisplay(
postSnapshot: snapshot.data!.docs[index],
currentUID: currentUID,
);
},
),
),
);
}
}),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
}),
);
}
}
I have thought of splitting the array into chunks/batches and doing queries that way (as seen in some of the other similar questions on here), or even flipping the database scheme upside-down by storing the IDs of the users into the post documents and using the array contains query as it is not limited. I am somewhat inclined against this, however, as I want the post documents to contain as little information as possible as they're the most often loaded element in the project- the way that the project is set up means that certain popular individual posts would most likely have more people attached to it as subscribers than the average user would have posts attached to it as entities he or she follows. No matter what the solution, the resulting list needs to be ordered (this is what kind of differentiates my problem from the other questions on the site I think) by time of last modification (using the field in the post document). I'm not sure if this requirement makes chunks/batching unfeasible. Please let me know if you have any suggestions on how to succeed. Thank you so much in advance for any help!

Flutter Async: Future.wait() constantly returning null

For some reason, Future.wait() is constantly returning null. I'm not completely certain I am using it correctly.
For context, I have a collection of posts in Firebase. For each post, I can extract the userID assigned to it, then for each post individually I use the userID of the poster to grab the username for display purposes. I grab the Post from a snapshot:
static Future<Post> fromSnapshot(QueryDocumentSnapshot<Object?> doc) async {
final _documentId = doc.id;
final _title = doc.get('title');
final _text = doc.get('text');
final _createdOn = doc.get('createdOn');
final _userID = doc.get('userID');
final userDoc = await FirebaseFirestore.instance.collection('users').doc(_userID).get();
final username = userDoc.get("username");
return Post(documentId: _documentId, title: _title, text: _text, createdOn: _createdOn, username: username);
}
and the extraction of posts occurs in a getPosts() function elsewhere:
Future<List<Post>> getPosts() async {
QuerySnapshot posts = await FirebaseFirestore.instance.collection('posts').get();
final allData = posts.docs.map(
(doc) async => await Post.fromSnapshot(doc)
).toList();
print(allData); // [Instance of 'Future<Post>', Instance of 'Future<Post>', Instance of 'Future<Post>']
final futurePosts = Future.wait(allData);
print(futurePosts); // Instance of 'Future<List<Post>>'
// why does this always return null?
return futurePosts;
}
the problem is it has to be async to extract the posts but also to get the username, meaning it returns a future list of future posts. I want to pass the result of getPosts() to a FutureBuilder, so I need a Future List of posts, and to not make all the posts Future I use Future.wait - but that always seems to return null. Essentially, I am mapping each post in the snapshot to its own Post item, where in the constructor it needs to run a further async call to extract the username. Am I missing something?
Note: even making the Future.wait() await returns null, it just also doesn't return a List of type Future so I can't use it in the FutureBuilder either.
Edit 1:
It turns out that futurePosts is actually an Instance of 'Future<List<Post>>', but when accessing the data within the FutureBuilder, snapshot.data is null:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Feed'),
),
body: FutureBuilder(
future: getPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
print(snapshot.data);
return postsToColumn(context, snapshot.data as List<Post>);
}
return const Center(
child: CircularProgressIndicator(),
);
}
),
);
}
Ok, lots of thanks to #IvoBeckers for helping me pin this down. It turns out, the snapshot actually did have an error, as they said, but this doesn't get printed unless you print it explicitly:
if (snapshot.hasError) {
print(snapshot.error.toString());
}
And the error is
Bad state: cannot get a field on a DocumentSnapshotPlatform which does not exist
So it turns out that not every User has a corresponding entry in the users collection with its username, which sounds like something I should have checked before, but I thought such an error would be printed out in the console. Once I updated the users collection, it worked perfectly.

Flutter error : The argument type 'List<Future<Widget>>' can't be assigned to the parameter type 'List<Widget>'

I'm trying to do a list of item from Firebase Firestore (this is done) and to get for each item a different image URL from Firebase Cloud Storage.
I use a function called getPhotoUrl to change the value of the variable photoUrl. The problem is that the return is executed before getPhotoUrl. If I add await in front of the function getPhotoUrl and async after _docs.map((document), I got an error saying that The argument type 'List<Future>' can't be assigned to the parameter type 'List'.
My code:
class PhotosList extends StatefulWidget {
#override
_PhotosListState createState() => _PhotosListState();
}
class _PhotosListState extends State<PhotosList> {
String photoUrl = 'lib/assets/default-image.png';
List<DocumentSnapshot> _docs;
getPhotoUrl(documentID) {
Reference ref = storage
.ref('Users')
.child(currentUser.uid)
.child('Photos')
.child(documentID)
.child('image_1.jpg');
ref.getDownloadURL().then((value) {
setState(() {
photoUrl = value.toString();
});
}).catchError((e) {
setState(() {
print(e.error);
});
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: firestore
.collection('Users')
.doc(currentUser.uid)
.collection('Photos')
.orderBy('date')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
_docs = snapshot.data.docs;
if (_docs.isEmpty)
return Center(
child: Text("The list is empty."));
return Container(
child: ResponsiveGridList(
desiredItemWidth: 100,
squareCells: true,
minSpacing: 5,
children: _docs.map((document) {
getPhotoUrl(document.id);
return PhotosListItem(photoUrl: photoUrl);
}).toList(),
),
);
},
);
}
}
I think you mix 2 different ways. In every build cicle you map your docs and request that photoUrl, but inside that method you call setState, which re-triggers your build method. That way you should end in infinite loop of getting photo url and building your widget.
You have three options:
Load your photoUrls and store them inside your widget -> call set state -> check inside your mapping function if your photo is loaded, if yes, take it, if no, call your getPhotoUrl function
Load your photoUrls synchronously and return url from your function and set it to your PhotosListItem
(I would prefer this) Add your documentId to your photosListItem in your mapping function and inside your item you load this photo url. In this PhotoListItem you have a variable with your imageUrl and in initState you call your getPhotoUrl function
Inside your PhotoItem:
String imageUrl;
#override
void initState() {
Future.delayed(Duration.zero, () {
setState(() {
// load your data and set it to your variable
imageUrl = ..
});
});
super.initState();
}
You might use a FutureBuilder because StreamBuilder seems to be synchronous :
How to convert Future<List> to List in flutter?
Thanks for your answers guys, actually I found an other solution which is to get and write the URL in Firestore directly after uploading the image on the Storage.
This is the article which helped me a lot : https://medium.com/swlh/uploading-images-to-cloud-storage-using-flutter-130ac41741b2
(PS: some of the Firebase names changed since this article but it's still helpful.)
Regards.

How to load list only after FutureBuilder gets snapshot.data?

I have a JSON method that returns a List after it is completed,
Future<List<Feed>> getData() async {
List<Feed> list;
String link =
"https://example.com/json";
var res = await http.get(link);
if (res.statusCode == 200) {
var data = json.decode(res.body);
var rest = data["feed"] as List;
list = rest.map<Feed>((json) => Feed.fromJson(json)).toList();
}
return list;
}
I then call this, in my initState() which contains a list hello, that will filter out the JSON list, but it shows me a null list on the screen first and after a few seconds it loads the list.
getData().then((usersFromServer) {
setState(() {. //Rebuild once it fetches the data
hello = usersFromServer
.where((u) => (u.category.userJSON
.toLowerCase()
.contains('hello'.toLowerCase())))
.toList();
users = usersFromServer;
filteredUsers = users;
});
});
This is my FutureBuilder that is called in build() method, however if I supply the hello list before the return statement, it shows me that the method where() was called on null (the list method that I am using to filter out hello() )
FutureBuilder(
future: getData(),
builder: (context, snapshot) {
return snapshot.data != null ?
Stack(
children: <Widget>[
CustomScrollView(slivers: <Widget>[
SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
child: puzzle[0],
);
},
childCount: 1,
),
)
]),
],
)
:
CircularProgressIndicator();
});
You are calling your getData method multiple times. Don't do that. Your UI waits for one call and your code for the other. That's a mess. Call it once and wait for that call.
You need to define the future to wait for in your state:
Future<void> dataRetrievalAndFiltering;
Then in your initstate, assign the whole operation to this future:
(note that I removed the setState completely, it's not needed here anymore)
dataRetrievalAndFiltering = getData().then((usersFromServer) {
hello = usersFromServer.where((u) => (u.category.userJSON.toLowerCase().contains('hello'.toLowerCase()))).toList();
users = usersFromServer;
filteredUsers = users;
});
Now your FurtureBuilder can actually wait for that specific future, not for a new Future you generate by calling your method again:
FutureBuilder(
future: dataRetrievalAndFiltering,
builder: (context, snapshot) {
Now you have one Future, that you can wait for.
This response is a little too late to help you, but for anyone wondering how to load a Future and use any of it's data to set other variables without having the issue of saving it in a variable, and then calling setState() again and loading your future again, you can, as #nvoigt said, set a variable of the Future in your state, then you can call the .then() function inside the addPostFrameCallback() function to save the result, and finally using the future variable in your FutureBuilder like this.
class _YourWidgetState extends State<YourWidget> {
...
late MyFutureObject yourVariable;
late Future<MyFutureObject> _yourFuture;
#override
void initState() {
super.initState();
_yourFuture = Database().yourFuture();
WidgetsBinding.instance?.addPostFrameCallback((_) {
_yourFuture.then((result) {
yourVariable = result;
// Execute anything else you need to with your variable data.
});
});
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _yourFuture,
builder: ...
);
}
}
dynamic doSomeStuffWithMyVariable() {
// Do some stuff with `yourVariable`
}
So, the advantage of this is that you can use the data loaded in the future outside the scope of the FutureBuilder, and only loading it once.
In my case, I needed to get a map of objects from my future, then the user could select some of those objects, save them on a map and make some calculations based on that selection. But as selecting that data called setState() in order to update the UI, I wasn't able to do so without having to write a complicated logic to save the user selection properly in the map, and having to call the future again outside the scope of the FutureBuilder to get it's data for my other calculations.
With the example above, you can load your future only once, and use the snapshot data outside the scope of the FutureBuilder without having to call the future again.
I hope I was clear enough with this example. If you have any doubts I will gladly clarify them.

How to inform FutureBuilder that database was updated?

I have a group profile page, where a user can change the description of a group. He clicks on the description, gets on a new screen and saves it to Firestore. He then get's back via Navigator.pop(context) to the group profile page which lists all elements via FutureBuilder.
First, I had the database request for my FutureBuilder inside the main build method (directly inside future builder 'future: request') which was working but I learnt it is wrong. But now I have to wait for a rebuild to see changes. How do I tell FutureBuilder that there is a data update?
I am loading Firestore data as follows within the group profile page:
Future<DocumentSnapshot> _future;
#override
void initState() {
super.initState();
_getFiretoreData();
}
Future<void> _getFiretoreData() async{
setState(() {
this._future = Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();});
}
The FutureBuilder is inside the main build method and gets the 'already loaded' future like this:
FutureBuilder(future: _future, ...)
Now I would like to tell him: a change happened to _future, please rebuild ;-).
Ok, I managed it like this (which took me only a few lines of code). Leave the code as it is and get a true callback from the navigator to know that there was a change on the second page:
// check if second page callback is true
bool _changed = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileUpdate(userId: globals.userId.toString())),
);
// if it's true, reload future data
_changed ? _getFiretoreData() : Container();
On the second page give the save button a Navigator.pop(context, true).
i would advice you not to use future builder in this situation and use future.then() in an async function and after you get your data update the build without using future builder..!
Future getData() async {
//here you can call the function and handle the output(return value) as result
getFiretoreData().then((result) {
// print(result);
setState(() {
//handle your result here.
//update build here.
});
});
}
How about this?
#override
Widget build(BuildContext context) {
if (_future == null) {
// show loading indicator while waiting for data
return Center(child: CircularProgressIndicator());
} else {
return YourWidget();
}
}
You do not need to set any state. You just need to return your collection of users in your GetFirestoreData method.
Future<TypeYouReturning> _getFirestoreData() async{
return Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();
}
Inside your FutureBuilder widget you can set it up something like Theo recommended, I would do something like this
return FutureBuilder(
future: _getFirestoreData(),
builder: (context, AsyncSnapshot<TypeYouReturning> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.length == 0)
return Text("No available data just yet");
return Container();//This should be the desire widget you want the user to see
}
},
);
Why don't you use Stream builder instead of Future builder?
StreamBuilder(stream: _future, ...)
You can change the variable name to _stream for clarity.