How to get list of document ids? - firebase

I am trying to retrieve a list of document Ids from firestore.
My firestore functions are stored as static methods in a FirebaseAPI class I created.
This is the function that should return the list of documentIds:
static List<String> avaiableDocuments() {
List<String> ids = [];
final Future<QuerySnapshot> snapshot = Firestore.instance.collection('admin').getDocuments();
snapshot.then((value) {
value.documents.forEach((element) {
ids.add(element.documentID);
});
});
return ids;
}
When I restart the app, the list of document ids do not appear.
However, they do appear when the is then hot reloaded.
My understanding is that when the app restarts, the list of strings is still empty.
This is because the page renders before the future returns.
Then when hot reload calls the build method again, the list is already populated so it displays it.
I tried adding "await" before the getDocuments call.
This turns the Future to just QuerySnapshot. Fair Enough.
Now "async" must be added to the function.
Then the function is now requiring a future return instead of a list.
Can't I just get a list from this function?
I tried to go with it and ended up with this:
static Future<List<String>> avaiableDocuments() async{
List<String> ids = [];
final QuerySnapshot snapshot = await Firestore.instance.collection('admin').getDocuments();
snapshot.documents.forEach((element) {
ids.add(element.documentID);
});
return ids;
}
How is it that I'm allowed to return a list from a function that says it's returning a future?
When I tried to consume the future in the screen class, same thing happens. List only appears on hot reload after restart.
Note: I feel it's a simple issue, but I pretty much a noob when it comes to asynchronous voodoo.
Class that should display the list of document Ids
import 'package:flutter/material.dart';
import 'package:hisab/database/firestore_api.dart';
class PreviousCarts extends StatefulWidget {
const PreviousCarts({Key key}) : super(key: key);
#override
_PreviousCartsState createState() => _PreviousCartsState();
}
class _PreviousCartsState extends State<PreviousCarts> {
List<String> documentIds = [];
#override
void initState() {
super.initState();
Future<List<String>> f = FirestoreAPI.avaiableDocuments();
f.then((value) {
value.forEach((element) {
documentIds.add(element);
});
});
}
#override
Widget build(BuildContext context) {
print(documentIds);
return Material(
child: Padding(
padding: const EdgeInsets.only(left: 8.0, top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: documentIds.map((e) {
return Card(child: ListTile(title: Text(e)));
}).toList(),
),
),
);
}
}

Can't I just get a list from this function?
No. Since the database query is asynchronous, everything that involves it must also be async. It's a bad idea to try to turn an async method in to an immediate return method, as that would cause your app to freeze if a query is slow or the app is offline.
I suggest reading this for some context. https://medium.com/firebase-developers/why-are-the-firebase-apis-asynchronous-e037a6654a93
How is it that I'm allowed to return a list from a function that says it's returning a future?
Because the dart compiler is making sure that the caller receives a Future. It's handling the hard stuff behind the scenes with respect to async/await to make it easier for you to write code, so that you don't have to be explicit about futures whenever they appear.
This style of asynchronous programming is very common, and comes up in other languages such as JavaScript. If you want to be effective with dart, and async programming in general, then I suggest taking time to become familiar with these techniques. It takes practice.

Related

How can I mix a statenotifier, with streamproviders that fetch 2 collections from Firebase to get constant update on a chat list

This is my first question here and I hope I’m not making it too complex.
So, I’m a junior programmer and I’ve start learning flutter, firebase and riverpod a couple of months ago and I’m stuck in a specific project.
For part of the app, what I need to do is something very similar to WhatsApp:
A screen with all the user chats,
This screen (or part of it) should update every time a chat gets a new message, showing the last message snippet and turning into bold if not read,
The shown chats should change between unarchived and archived, depending on a button in the UI.
Couldn’t have picked up an easier starting project, right? ;) (now every time I look at WhatsApp I say wow!)
Regarding firebase/firestore I’m fetching 2 different collections for this:
the sub-collection ‘chats’ within the ‘user_chats’ collection: where I get all the chat Ids plus it’s status (if this chat is archived and if the last message was read), for the current user,
the main ‘chats’ collection, where I have the main info of each chat.
At this moment I’m doing this:
In the chats_screen (UI) I’m fetching my chat provider: userChatsProvider
(note: this is just an example of the UI implementation. I have another implementation for it, but as long I get the chatName and lastMsgContent updated for each chat, perfect.)
class ChatsScreen extends ConsumerWidget {
const ChatsScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
(…)
return Scaffold(
appBar: (…) // not relevant for this question
body: Center(
child: Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
return ref.watch(userChatsProvider).when(
loading: () => const CircularProgressIndicator(),
error: (err, st) => Center(child: Text(err.toString())),
data: (chatData) {
return Column(
children: [
// button to get archived chats
child: TextButton(
child: (…)
onPressed: () {}),
),
Expanded(
child: ListView.builder(
itemCount: chatData.length,
itemBuilder: (ctx, index) => Row(
children: [
Text (chatData[index].chatName!),
Text (chatData[index].lastMsgContent!),
]
),
),
),
]
);
}
);
}
)
)
);
}
}
In the chats_provider (provider) I’m fetching the 2 repository providers and joining them into a specific model I’ve created for this screen:
final userChatsProvider = FutureProvider.autoDispose<List<ChatScreenModel>>((ref) async {
const String userId = ‘XXX’; // this will be substituted by a dynamic variable with the current user Id
try {
final List<UserChat> userChats =
await ref.watch(userChatsRepositoryProvider).get(userId: userId);
final List<String> chatIdList = userChats.map<String>((e) => e.id).toList();
final List<Chat> chats =
await ref.watch(chatsRepositoryProvider).get(chatIds: chatIdList);
// ref.maintainState = true;
return toChatScreenModel(chats, userChats);
} on Exception catch (e) {
throw const CustomException();
}
});
In the repositories I’m fetching the firestorm collections I mentioned above. Here’s an example:
final userChatsRepositoryProvider =
Provider<UserChatsRepository>((ref) => UserChatsRepository(ref.read));
class UserChatsRepository {
final Reader _read;
const UserChatsRepository(this._read);
Future<List<UserChat>> get({required String userId}) async {
try {
final snap = await _read(firebaseFirestoreProvider)
.collection('users_chats/$userId/chats')
.get();
// Maybe this step is not necessary, but I’ve decided to transform the data into temporary models, before sending to provider
List<UserChat> userChats =
snap.docs.map((doc) => UserChat.fromJson(doc.data(), doc.id)).toList();
return userChats;
} on FirebaseException catch (e) {
throw CustomException(message: e.message);
}
}
}
And by the way, this is the model I’m sending to the UI:
class ChatScreenModel {
final String? id;
final String? chatName;
final String? chatImage;
final String? lastMsgContent;
final String? lastMsgDate;
final ChatType? chatType;
final bool? archived;
final bool? msgRead;
ChatScreenModel({
this.id,
this.chatName,
this.chatImage,
this.lastMsgContent,
this.lastMsgDate,
this.chatType,
this.archived,
this.msgRead,
});
Problems with this implementation:
I’m getting the user chats in the screen, but they don’t update since I’m not using a stream. So I get a snapshot, but it will only update if I leave and enter that chats_screen again. And it would be important to have it updating with a stream.
I’m showing all the chats, and not a filtered list with only the unarchived chats.
Also, related with the previous point, I still don’t have the archived button working, to only show the archived chats.
I’ve lost many, many hours trying to understand how I could implement a stream provider and a state notifier provider in this workflow.
Tried many combinations, but without success.
Can anyone help me understand how to do this?
Priority: transform these providers into stream providers (so it updates the UI constantly).
Nice to have: also include the archived/unarchived dynamic to filter the chats that appear and be able to switch between them.
Thanks a lot. :)

Does Flutter Streambuilder use cache on setState or retrieve all documents again from the firestore database?

I am building a chat functionality for my Flutter application.
To ensure that the latest message of a user is always showed, I need the real-time functionality of a Streambuilder.
However, I need to introduce pagination as well to avoid that all chat messages from the database are loaded from the database each time the Widget is rebuilt.
I've implemented this behaviour successfully with the code below. Each time the maximum scrollExtent is reached, the Widget is rebuilt with a higher documentLimit.
My question is now the following:
When setState is called, will the data from the streambuilder that was already there be read again from the database? Or will the Streambuilder use its cache where possible and only use reads for the documents that it does not have?
In other words, with a documentLimit of 20, will each set state only cost me maximum 20 extra reads? Or will it cost me the entire new documentLimit?
void initState() {
maxMessageToDisplay = 20;
_scrollController = ScrollController();
_scrollController.addListener(() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
setState(() {
maxMessageToDisplay += 20;
});
}
});
super.initState();
}
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _firestore.collection('chat').limit(maxMessageToDisplay).orderBy('timestamp', descending: true).snapshots(),
builder: (context, snapshot) {
final messages = snapshot.data.documents;
messages.sort((a, b) => b.data['timestamp'].compareTo(a.data['timestamp']));
var format = new DateFormat("Hm");
List<MessageBubble> messageBubbles = [];
for (var message in messages) {
final messageText = message.data['text'];
final messageSender = message.data['sender'];
final messagePhoto = message.data['photo'];
final messageUserId = message.data['uid'];
final messageTime = format.format(DateTime.fromMillisecondsSinceEpoch(message.data['timestamp'], isUtc: false));
final messageBubble = MessageBubble(
sender: messageSender,
text: messageText,
photo: messagePhoto,
time: messageTime,
userId: messageUserId,
);
messageBubbles.add(messageBubble);
}
return Expanded(
child: ListView(
controller: _scrollController,
reverse: true,
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
children: messageBubbles,
),
);
},
);
}
}
By changing the limit you create a whole new query and all data for it is loaded as it is for each new query.
To avoid loading all data you could implement a pagination as shown here. Where you would use the last loaded doc and startAfter to load the next 20 elements from the point where the previous query stoped. It would make your code more complicated to handle multiple realtime queries and keep track of the last docs.
You could also consider to have only the last 20 messages in realtime and if you need the older ones to get them with the get call only once because they would probably not change. Make sure to store all messages from the listener and get calls into a single List and make sure not to store duplicates accidentaly.
I would also consider if it is so importand to avoid loading all data when watching older messages. Does that happen so often in your app? I have multiple chat apps with Firebase that use the same logic with just increasing the limit. Users very rarely watch older messages so implementing such workflows with pagination would not have much effect on the billing and total read amount.

Flutter FutureBuilder throws error, but on simulation it works as expected

I'm a beginner to Flutter and currently I'm having a problem with correctly implementing FutureBuilder in flutter.
I'm trying to build a user page, where I have my user information stored in Firebase, and every time I access the user page it retrieves the current user data and shows the data on the page. Here's the code I've written for the implementation:
class UserPage extends StatefulWidget{
#override
UserPageState createState() => UserPageState();
}
class UserPageState extends State<UserPage>{
String userName;
String userEmail;
String collegeName;
Future _infoInit() async {
userName = await HelperFunctions.getUserNamePreference();
userEmail = await HelperFunctions.getUserEmailPreference();
collegeName = await HelperFunctions.getUserCollegePreference();
}
Widget userScaffold(BuildContext context, AsyncSnapshot snapshot){
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(
userName
),
backgroundColor: Colors.lightBlue,
),
body:Center(
child: Text("This is User Page")
)
);
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _infoInit(),
builder: (context,AsyncSnapshot snapshot) => userScaffold(context, snapshot)
);
}
}
At the moment the only part I've written is showing the currently logged in user on the app bar, and when I run the code, it seems that it's successfully running. However, when I look at the android studio console I could see that it's actually facing and error which I think is related to async function executed in the FutureBuilder widget.
Error Message:
Performing hot reload...
Syncing files to device iPhone 11...
Reloaded 7 of 650 libraries in 397ms.
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building FutureBuilder<dynamic>(dirty, state: _FutureBuilderState<dynamic>#772f1):
A non-null String must be provided to a Text widget.
'package:flutter/src/widgets/text.dart':
Failed assertion: line 298 pos 10: 'data != null'
The relevant error-causing widget was:
FutureBuilder<dynamic> file:///Users/nossu3751/Downloads/flutter_project/moim_app/lib/user/profile.dart:39:12
When the exception was thrown, this was the stack:
#2 new Text (package:flutter/src/widgets/text.dart:298:10)
#3 UserPageState.userScaffold (package:moimapp/user/profile.dart:26:18)
#4 UserPageState.build.<anonymous closure> (package:moimapp/user/profile.dart:41:54)
#5 _FutureBuilderState.build (package:flutter/src/widgets/async.dart:732:55)
#6 StatefulElement.build (package:flutter/src/widgets/framework.dart:4619:28)
More exactly, it says that the userName that I'm trying to use in the FutureBuilder is null, even though I believe I assigned the value already through running _infoInit() method and it does actually show correctly on the simulator.
I would really appreciate it if someone can let me know what I'm doing wrong here, and what I can do to stop this message from appearing again. Thank you very much in advance!
The problem is that the FutureBuilder's builder method is invoked each time the AsyncSnapshot is changed (and, initially, the snapshot has no data). Therefore, in the first couple of times that builder is called, userName will be null, thus giving you that error; but after some point, the username will have been fetched, and when the builder function is called you'll see the username on the screen correctly.
The idiomatic way to use FutureBuilder is the following:
FutureBuilder(
future: myFuture,
builder: (context, AsyncSnapshot snapshot) {
// Try adding this print to your code to see when this method is being executed!
print('Building with snapshot = $snapshot');
if (snapshot.hasData) {
// return widget with data - in your case, userScaffold
}
else if (snapshot.hasError) {
// return widget informing of error
}
else {
// still loading, return loading widget (for example, a CircularProgressIndicator)
}
},
);
So, initially, the builder function will be called will be with a snapshot without data (the "else" branch). You'll probably want to show a loading widget in that case. Then, after some time, the Future completes, and the builder function is called with a snapshot having either the data or some error.
Another important thing in your code is that your function _infoInit doesn't actually return anything. So, in fact, your FutureBuilder isn't using the data from the AsyncSnapshot (which means that the above snippet will not actually work, since snapshot.hasData will never be true). With FutureBuilder, you normally want to build the widget using the data returned by the AsyncSnapshot. Instead, what happens in your code is:
FutureBuilder is created. This calls _infoInit(), which triggers fetching the data from Firebase;
FutureBuilder's builder method is called. It tries using userName, but it is null so Flutter shows the failed assertion;
_infoInit() fetches all the data, and returns a Future (this future is returned automagically because of the async clause in the method signature; however, without a return clause, it doesn't actually return any data). But despite the Future not having any data, the 3 variables in the state (including userName) have been updated, and now contain some data.
Since the future passed to the FutureBuilder has completed, the builder method is called again. This time, userName has data, so it builds correctly.
It's fine to write the code as you did, but in that case, you don't need to use a FutureBuilder. You could just invoke _infoInit() from the widget's initState() method (initState is a method called once when the State is first built) and, after the data is fetched, call setState(). Here's how that would look:
class UserPage extends StatefulWidget {
#override
UserPageState createState() => UserPageState();
}
class UserPageState extends State<UserPage> {
String userName;
String userEmail;
String collegeName;
bool loadingData = true;
#override
void initState() {
_infoInit();
}
// This can be void now, since we're changing the state, rather than returning a Future
void _infoInit() async {
String userName = await HelperFunctions.getUserNamePreference();
String userEmail = await HelperFunctions.getUserEmailPreference();
String collegeName = await HelperFunctions.getUserCollegePreference();
setState(() {
// In theory, we could have just updated the state variables above, but the
// recommended practice is to update state variables inside setState.
this.userName = userName;
this.userEmail = userEmail;
this.collegeName = collegeName;
loadingData = false;
});
}
Widget userScaffold(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(userName),
backgroundColor: Colors.lightBlue,
),
body: Center(child: Text("This is User Page")));
}
#override
Widget build(BuildContext context) {
if (loadingData) {
// We don't have the data yet, so return a widget to indicate some loading state
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return userScaffold(context);
}
}
The above snippet doesn't treat for errors in fetching the data, which you'll probably want to do. And to do that, you might use a flag called 'hasError' or something - which ultimately, will give very similar code to how the "idiomatic" FutureBuilder builder method is written.
Both are valid approaches; FutureBuilder maybe uses less code (and might be simpler to use if the rest of your code already uses Futures), but ultimately, it's up to your preference.
You need to use ConnectionState inside your builder. Look at this code template: (Currently your builder return userScaffold without waiting for the future to complete)
return FutureBuilder(
future: yourFuture(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// future complete
// if error or data is false return error widget
if (snapshot.hasError) {
return _buildErrorWidget('SOMETHING WENT WRONG, TAP TO RELOAD');
}
// return data widget
return _buildDataWidget();
// return loading widget while connection state is active
} else
return _buildLoadingWidget();
},
);

Returning null user data from Firestore. How to reference it globaly instead?

I'm quite new to Flutter and I've been struggling to access a user's document on Firestore.
On the profile page,
I'm setting the current user's UID inside initState, but uid returns null for a quick second, then the page updates with correct info.
So I am able to retrieve a certain field (like displayName), but it isn't quite the best practice. I don't want to have a bunch of boilerplate code and await functions mixed with UI and such.
Code:
FirebaseUser user;
String error;
void setUser(FirebaseUser user) {
setState(() {
this.user = user;
this.error = null;
});
}
void setError(e) {
setState(() {
this.user = null;
this.error = e.toString();
});
}
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then(setUser).catchError(setError);
}
Then in my body I have a Stream builder to get the document.
body: StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(user.uid)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.deepOrange),
),
);
} else {
var userDocument = snapshot.data;
return showProfileHeader(userDocument);
}
},
)
I want to make 'global' references to be accessed throughout the app. Instead of getting the user's id on every page and streaming a specific field when I might need multiple ones.
The only ways I found online to do something similar, created lists with all the data in it. I feel like this might get extra fields I don't need.
How can I make data from Firestore available across the app?
I am using the "Provider" package for doing state management across my app. Nowadays its also the suggested way by the google flutter team when it comes to state management. See the package here: https://pub.dev/packages/provider
Regarding Firebase Auth and accessing the credentials application wide, i am using that said package like stated on this page:
https://fireship.io/lessons/advanced-flutter-firebase/
Short version below. Bootstrap your app like so:
import 'package:provider/provider.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Make user stream available
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged),
// not needed for your problem but here you can see how
// to define other Providers (types) for your app.
// You need a counter class which holds your model of course.
ChangeNotifierProvider(builder: (_) => Counter(0)),
],
// All data will be available in this child and descendents
child: MaterialApp(...)
);
}
}
Then in your child widgets, just do:
// Some widget deeply nested in the widget tree...
class SomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
var user = Provider.of<FirebaseUser>(context);
return Text(user.displayName) // or user.uid or user.email....
}
}
This should do the trick.
That happens because FirebaseAuth.instance.currentUser() returns a future, and until that future is completed, you will not have the proper FirebaseUser object.
Making the user object global is not a bad idea. In addition, you can hook it up to the FirebaseAuth stream so that it gets updated everytime the user auth status changes, like so in a user.dart file:
class User {
static FirebaseUser _user;
static get user => _user;
static void init() async {
_user = await FirebaseAuth.instance.currentUser();
FirebaseAuth.instance.onAuthStateChanged.listen((firebaseUser) {
_user = firebaseUser;
});
}
}
You can call User.init() in main() and access the user object with User.user.

What's the best way to iterate through documents on a Firestore collection summing values?

I'm trying to find out how to sum a value stored on every document inside a Firestore colletion, using the "cloud_firestore" package from Flutter.
I tried this code:
double queryValues() {
total = 0.0;
Firestore.instance
.collection('myCollection')
.snapshots()
.listen((snapshot) {
snapshot.documents.forEach((doc) => this.total +=
doc.data['amount']);
});
debugPrint(this.total.toString());
return total;
}
But I end up printing 0.0 as result. If I remove the first line (total = 0.0.) I get the sum to work, but if I reload the values are doubling (adding up to the old total).
I known they say the best solution is to store this aggregated value using a Cloud Function on every document write, but that would be a pain as I want to make totals by some range periods available for the user to query (month, year, week). Those queries result on a small number of documents, so for me it would be ideal just to iterate over the resulting collection).
This is my first question posted and I really appreciate any thoughts on this.
If you are using an async function either you do all your work inside the listen() method or wait outside it til you are sure everything has finished inside.
The "Flutter" way of handling the second case is using setState(). You build() the widget tree with the initial outside value and when the async function finishes you call setState() so the widget tree is rebuilt with the new value.
(As a side note, inside the async function you could use the fold() list method that accumulates on the previous operation).
Here is a complete simple app that would use the external total value:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double total = 0.0;
#override
initState() {
super.initState();
queryValues();
}
void queryValues() {
Firestore.instance
.collection('myCollection')
.snapshots()
.listen((snapshot) {
double tempTotal = snapshot.documents.fold(0, (tot, doc) => tot + doc.data['amount']);
setState(() {total = tempTotal;});
debugPrint(total.toString());
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text("Total: $total")),
);
}
}
CollectionReference _documentRef=Firestore.instance.collection("User");
_documentRef.getDocuments().then((ds){
if(ds!=null){
ds.documents.forEach((value){
print(value.data);
});
}
});
As Doug commented, data is loaded from Firestore asynchronously. By the time you print the sum, data hasn't loaded yet, the forEach hasn't run yet, and the total will still be 0.0.
It's easiest to see what's happening by placing a few print statements:
print("Before calling listen");
Firestore.instance
.collection('myCollection')
.snapshots()
.documents((snapshot) {
print("Got data");
}
print("After calling listen");
When you run this code, it prints:
Before calling listen
After calling listen
Got data
This is probably not the order you expected. But it explains why you get no result and a sum of 0.0: the data hasn't loaded yet.
To solve this you need to use Dart's async/await keywords.
double queryValues() async {
total = 0.0;
docs = await Firestore.instance
.collection('myCollection')
.snapshots()
.documents((snapshot);
docs.forEach((doc) => this.total += doc.data['amount']));
debugPrint(this.total.toString());
return total;
}
You'll notice I also replaced listen() with get(), since you only want to get the documents once (a listen() will stay active, which means you can't return a result).
And then call it with:
sum = await queryValues();
print(sum)

Resources