My data is set up as follows:
- The Users collection contains User documents.
- A User document contains a Friends subcollection.
- A Friends subcollection contains UserRef documents.
- A UserRef document contains a DocumentReference to a User document.
I want to display all the friends of a particular user in a RecyclerView using the FirestoreRecyclerAdapter. From this answer, it seems like I cannot retrieve the User documents pointed to by a DocumentReference in a query. So, the following code is my attempt at using a SnapshotParser to do so. However, I don't know how to can return the User from parseSnapshot() since it is retrieved asynchronously.
Query query = friendsSubcollection;
FirestoreRecyclerOptions<User> options = new FirestoreRecyclerOptions.Builder<User>()
.setQuery(query, User.class, new SnapshotParser<User>() {
#NonNull
#Override
public User parseSnapshot(#NonNull DocumentSnapshot snapshot) {
DocumentReference userRef = snapshot.toObject(DocumentReference.class);
userRef.get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
User user = documentSnapshot.toObject(User.class); // How do I return this user from parseSnapshot()?
}
});
}
})
.build();
You won't be able to do this because parseSnapshot needs to happen synchronously, but getting another document is asynchronous. The document get won't complete with a callback until after parseSnapshot returns. So, you won't be able to use Firebase UI for this - you'll have to come up with your own way of making multiple requests to populate the RecyclerView, and it will likely require a lot of work.
Related
I would like to update user data in provider if user document has changed on firestore.
Actually, I use provider to store current user data in a variable called _currentUser. This variable is helpful to display user data on several screens.
class UserService extends ChangeNotifier {
final CollectionReference usersCollection =
FirebaseFirestore.instance.collection('users');
late User _currentUser;
User get currentUser => _currentUser;
Future<User> getUser(String uid) async {
var userData = await usersCollection.doc(uid).get();
User user = User.fromMap(userData.data()!);
_currentUser = user;
print("nb lives : ${_currentUser.nbLives}");
notifyListeners();
return user;
}
}
Current User data can change over time and the problem of my current solution is that if user document has changed, _currentUser variable is not updated and the old data is displayed on app screens. I want to find a way to listen to this document and update _currentUser variable if user data has changed.
A solution i found is to use Streams to fetch user data but I don't like it because it runs on a specific screen and not in background.
Does anyone have ever face a similar issue ? Thank you for your help !
class UserService extends ChangeNotifier {
final CollectionReference usersCollection =
FirebaseFirestore.instance.collection('users');
late User _currentUser;
User get currentUser => _currentUser;
void init(String uid) async {
// call init after login or on splash page (only once) and the value
// of _currentUser should always be updated.
// whenever you need _currentUser, just call the getter currentUser.
usersCollection.doc(uid).snapshots().listen((event) {
_currentUser = User.fromMap(event.data()!);
notifyListeners();
});
}
}
You can do this in many ways
When you update the _currentUser in firestore, update the same in the provider variable with notifylistner and wrap your widget that uses the _currentUser in Consumer, so the changes get updated always.
In you root widget use StreamBuilder with stream: ....snapshots() and on change update the _currentUser
This depends on your use-case and want things to react on changes to _currentUser.
I have the following code piece in a function that I call when I need to fetch the user profile data.
FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Future getUserProfile() async {
try {
DocumentSnapshot ds =
await _firestore.collection('users').doc(_auth.currentUser.uid).get();
return ds;
} catch (e) {
print(e);
return null;
}
}
With the assumption that user's profile data does not change, does this call cost me 1 read each time I call the getUserProfile() function? If yes, how can I change this function so that my function only listens to changes and does not necessarily increase the number of reads in firestore?
Yes, each function call will cost you 1 read. If the data does not change through out the app you could fetch it at the start of your application and store it by creating a class say User, then add data to that User object. This method is very useful and would minimize the number of function calls made to fetch data.
Each time you call getUserProfile(), the read counter will increase by one.
Firestore offers an alternative to get() for listening to real time changes. It's called snapshots(), it returns a Stream<QuerySnapshot>. You can attach a listener and every time one of the documents you listen to, changes, it will be added to the stream. Initially all items matching your query (in your case it's only one) will be added to the stream. Your code should be then:
Stream<QuerySnapshot> getUserProfile() {
try {
Stream<QuerySnapshot> stream = _firestore
.collection('users')
.doc(_auth.currentUser.uid)
.snapshots();
return stream;
} catch (e) {
print(e);
return null;
}
}
Every time a change is added to the stream, the read will be increased by one.
More information can be found at the official Firestore Docs.
I have a document with the totalUnpaid amount of a user.
I'm listening to this document with a StreamBuilder.
The stream is a function that returns Stream.
When I register a new user, I create the totalUnpaid document with a field of int 0.
Then immediately after, I navigate to a home page that shows the current totalUnpaid amount.
Previously, I've hard-coded the current user as a string.
Now that I've implemented Firebase authentication, I want the home page to show the totalUnpaid of the new user.
Previously, my stream was this:
static Stream<DocumentSnapshot> getTotalUnpaid(String currentUser) {
final doc = Firestore.instance.collection(currentUser).document('totalUnpaid');
return doc.snapshots();
}
Now I've created this method to get my current user:
static Future<FirebaseUser> getCurrentUser() async {
final FirebaseUser user = await FirebaseAuth.instance.currentUser();
return user;
}
How can I consume this Future inside my stream?
I will have to use an await, which will require the function to be async which will require the Stream to become a future which will require my StreamBuilder to become a FutureBuilder.
This won't be ideal, since the totalUnpaid needs to update every time the user adds an item.
You can do the following:
static Stream<DocumentSnapshot> getTotalUnpaid() async*{
final FirebaseUser user = await FirebaseAuth.instance.currentUser();
final doc = Firestore.instance.collection(user.uid).document('totalUnpaid');
yield* doc.snapshots();
}
When you mark a function as async* then you can use yield* which will return the values from the Stream.
The above will work assuming you are using the uid as a collection name.
I am not referring to firestore offline persistence, but a way to permanently cache documents that will survive closing the database connection and app. I want to cache entire documents.
For example, in a simple chat app. Say there are 100 messages in the conversation, and the user has already read them all. A new message gets sent, so the user opens the app to read the new message. To re-download all 100 messages from firestore will have you charged for 100 document reads. But since the user has already read and retrieved those, I want them cached locally and not read again from the database (since a chat message will never change once created). I understand pagination could help, but I'd rather not read the same static document more than once.
Is SQFlite the best option for this cross-platform, or is there something even better?
For a chat application I'd typically keep track of what the last message is that the user has already seen. If you show the messages ordered by their timestamp, that means you'll need to keep the timestamp of the latest message they've seen, and its document ID (just in case there are multiple documents with the same timestamp). With those two pieces of information, you can request only new documents from Firestore with collection.startAfter(...).
I recommend to save the last time the user made login in the device locally, an then use it to only get the messages them didn't receive.
Here is an oversimplified example:
import 'package:cloud_firestore/cloud_firestore.dart';
/// This class represents your method to acess local data,
/// substitute this class for your method to get messages saved in the device
/// I highly recommend sembast (https://pub.dev/packages/sembast)
class LocalStorage {
static Map<String, Message> get savedMessages => {};
static DateTime get lastLogin => DateTime.now();
static void saveAllMessages(Map<String, Message> messages) {}
static void saveLastLogin(DateTime lastLogin) {}
}
class Message {
String text;
String senderId;
DateTime timestamp;
Message.fromMap(Map<String, dynamic> map) {
text = map['text'] ?? 'Error: message has no text';
senderId = map['senderId'];
timestamp = map['timestamp'];
}
Map<String, dynamic> toMap() {
return {
'text': text,
'senderId': senderId,
'timestamp': timestamp,
};
}
}
class User {
DateTime lastLogin;
String uid;
Map<String, Message> messages;
void updateMessages() {
this.messages = LocalStorage.savedMessages;
this.lastLogin = LocalStorage.lastLogin;
/// Listening to changes in the firestore collection
final firestore = Firestore.instance;
final ref = firestore.collection('users/$uid');
final query = ref.where('timestamp', isGreaterThan: this.lastLogin);
query.snapshots().listen((querySnapshot) {
/// Updating messages in the user data
querySnapshot.documents.forEach((doc) {
messages[doc.documentID] = Message.fromMap(doc.data);
});
/// Updating user last login
this.lastLogin = DateTime.now();
/// Saving changes
LocalStorage.saveAllMessages(this.messages);
LocalStorage.saveLastLogin(this.lastLogin);
});
}
}
I have a firestore table of users, and another firestore table of blogs which has uid on each document. I would like to do join query in flutter by joining the two tables by their uid. I can't find a way of doing this in the fire quiz state management. I wanted to define a new class class.
BlogData<T> {
final Firestore _db = Firestore.instance;
final String path;
CollectionReference ref;
BlogData({ this.path }) {
ref = _db.collection(path);
}
Future<List<T>> getData() async {
// get blog data and do a join here to a user document by uid (document/blog.uid) return a future of blogs with user data such as name, country added to each blog in the future list.
}
Stream<List<T>> streamData() {
// get blog data and do a join here to the user document by uid (document/blog.uid) return a stream of blogs with user data such as name, country added to each blog in the stream.
}
}
How can I achieve this?