Firebase FireStore # Android - best practices for offline first - android

I am using Firebase Firestore (Not the realtime database) and programming an android app with it.
I want my app to work as offline first, which means that when the app loads FireStore will load the data from the Chace and show it to the user, and after that query changes from the server.
To accomplish that, I am doing a normal GET request (Not listener) with source of Chace.
After the get call has completed, I am attaching a listener to listen for all the update. (Locally or from the server), so I Can present the user the updated status.
The problem with this method is that basiclly it means I will download the same object twice First time on the initial get request, and than on the initial listener. Which means I will send duplicates to the UI.
Is there any practice available in the firestore API to avoid this? So the listener will not return records that the get request has already synced?
See below example code. Thx!
GET request: (runs first)
db.collection(sCollection)
.get(SOURCE.CHACE)
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
Log.d(TAG, document.getId() + " => " + document.getData());
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
Listener: (runs second)
m_registration = db.collection(sName).
orderBy("stamp", Query.Direction.ASCENDING)
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot value,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
for (DocumentChange dc : value.getDocumentChanges()) {
}
catch(JSONException ex) {
}
}

The Firebase Firestore handles offline so good that you can use it as oofline first database without doing anything on your own. You can read, listen and edit data as if you are doing it synchornously. You should deffinitely check this out.
That means you don't need to use get to make the offline better. It will work seamlesly with the listener to.

Related

How to connect firebase firestore to realtime database?

I made an application that users can earn points by taking quizzes. I have a leader board too.
my primary database is cloud firestore. but I need the leaderboard to be more real-time like it needs to update every time when a user earns points without refreshing or closing the fragment.
So I need to connect firebase firestore to real-time databases, (if I change the firestore data(like coins for the specified user or any), it needs to change the real-time data too)
I made codes but it didn't work well. I have attached the code here.
private void LoadFirestore() {
firebaseFirestore.collection("Users")
.document(FirebaseAuth.getInstance().getUid())
.get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
user = documentSnapshot.toObject(User.class);
totalCoins.setText(String.valueOf(user.getCoins()));
}
});
}
private void uploadToRealtime() {
HashMap<String, Object> map = new HashMap<>();
map.put("coins", totalCoins);
firebaseDatabase.getReference().child("Users").child(FirebaseAuth.getInstance().getUid())
.updateChildren(map);
}
}
You can use a onSnapshotListener to get the fata directly from firestore in realtime. Here is a basic example for that:
final DocumentReference docRef = db.collection("cities").document("SF");
docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
if (snapshot != null && snapshot.exists()) {
Log.d(TAG, "Current data: " + snapshot.getData());
} else {
Log.d(TAG, "Current data: null");
}
}
});
You can check more about it here.
Creating a sync to the RealtimeDatabase just for the realtime feature wouldn't make any sense here and would give you more costs in your Firebase project.

Delete all documents in Firestore collection

I made a button that is supposed to let users delete all their data based on their userid, the button has the same purpose like a "delete your account" button, but it is not working. I tried to delete all documents in the collection using this code:
final String userid = FirebaseAuth.getInstance.getCurrentUser().getUid();
db.collection("main").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
for (QueryDocumentSnapshot queryDocumentSnapshot : task.getResult()){
db.collection("main").document(userid).delete();
}
}
})
There is a runtimeExecution : no permission error
com.google.android.gms.tasks.RuntimeExecutionException: com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions.
I set the security rules as below so that no other users could access other users' data.
service cloud.firestore {
match /databases/{database}/documents {
match /main/{userId}/{document=**} {
allow read, write: if request.auth.uid == userId;
}
}
}
Not sure what to do, I thought the security rules could prevent security issues but I think it is causing this permission error?
Thank you in advance.
Update:
After doing some research and read so many other answers on this site about this matter, I used this method and I think it is so simple for a dummy like me but never mentioned on other answers I am not sure why:
To remove a user completely from authentication, I used the Firestore extension called "Delete User Data" which is a great help, it is magical and allows me to remove a user just by using this code:
FirebaseAuth.getInstance().getCurrentUser().delete().addOnSuccessListener(new OnSuccessListener<Void>() {...}
It doesn't only delete user authentication, with just that code, that particular user's data in the firestore and the images in the storage are also gone. It is fantastic!
The important point is that you have to connect the user UID from the authentication and connects the UID to the firestore and the storage.
To know more about how to use the "Delete User Data" Firestore extension(it is still in beta mode), try looking at this great blog tutorial from JORGE VERGARA : Installing the Delete User Data extension
How I set the configuration of the extension to delete a user:
How to use the extension: It is so simple I am so happy that it helps me to delete a user so easily without fuss
This is the storage structure:
The structure of my Firestore:
main(collection)--->userID---->journal(collection)----->journalIDdsvsfbsf
----->journalIDdfvdbgnd
--->userID2--->journal(collection)----->journalIDdsvsfbsf
----->journalIDdfvdbgnd
The Authentication structure:
See how the firestore and storage have to be connected with the same user UID so that the Firestore extension could delete a user easily by deleting the authentication.
If you still want to use the old method:
I noticed that you cannot delete a firestore document that contains one or multiple sub-collections with just delete method. For example, if you want to delete a document that contains 3 sub-collections, in order to delete that document(a user in my case), you have to get all the documents in one of the sub-collection, delete them all, then proceed to do the same to all the other sub-collections in that document. Once all the sub-collections in the document has been deleted, then the document that contains them will be gone itself. That's how Firestore works it seems.
To put it in code:
to delete a document that contains a sub-collection "journal"
Without using the extension, I have to do it like this:
FirebaseFirestore.getInstance().collection("main").document(userid).collection("journal").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (final QueryDocumentSnapshot document : task.getResult()) {
if (document.exists()){
db.collection("main").document(userid).collection("journal").document(document.getId()).delete().addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(SettingsActivity.this, "Journal: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
} else {
Toast.makeText(SettingsActivity.this, "No data in JOURNAL", Toast.LENGTH_SHORT).show();
}
}
} else {
Toast.makeText(SettingsActivity.this, "Journal task: " + task.getException(), Toast.LENGTH_LONG).show();
}
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(SettingsActivity.this, "Error getting all the documents: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
If you have multiple sub-collections in that particular document, to delete it you have to repeat that code multiple times for different sub-collections which will make the codes very long.
I hope this is helpful for someone out there who is trying to delete a user from Firestore. :) Use the extension already.
This query is trying to get all documents in the collection called "main":
db.collection("main").get()
Your security rules don't allow that. The rules only allow a user to read and write their own document in main.
It's not clear what exactly you're trying to delete, but if it's just the user document under main, you don't need a query at all. Just do this:
db.collection("main").document(userid).delete();
If you want to access all the documents under main collection you should change your Firebase rules to
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
Although if you want to delete only single user data which you are doing in your code here :-
db.collection("main").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
for (QueryDocumentSnapshot queryDocumentSnapshot : task.getResult()){
db.collection("main").document(userid).delete();
}
}
})
You shouldn't right the whole query you can simply delete the user document using
db.collection("main").document(userid).delete();
There is no API to delete an entire collection (or its contents) in one go.
From the Firestore documentation:
https://firebase.google.com/docs/firestore/manage-data/delete-data
"To delete an entire collection or subcollection in Cloud Firestore, retrieve all the documents within the collection or subcollection and delete them. If you have larger collections, you may want to delete the documents in smaller batches to avoid out-of-memory errors. Repeat the process until you've deleted the entire collection or subcollection."

How to add new fields in Firestore Document from two different sources simultaneously?

When a new user registers in my app using firebase-authentication custom sign in using email and password, I need to update that data into my firestore.
But Firebase only has FirebaseAuth.createUserWithEmailAndPassword(email, password) to create a new account and hence I cannot update my user's username at the same time.
To update the E-Mail in Firestore, I use Firebase cloud functions. Here's the code:
export const onNewUserJoined = functions.auth.user().onCreate((user) => {
//const newUserDisplayName = user.displayName //CAN'T USE THIS. REASON is BELOW
const newUserUID = user.uid
const newUserEmail = user.email
const timeCreated = Date.now()
console.log(`${newUserUID} has joined.`)
return admin.firestore().collection('Agent').doc(`${newUserUID}`).set({"E-Mail": newUserEmail, "Time": timeCreated})
})
OK, great now I have updated the E-Mail and time created in Firestore successfully.
But next challenge is I need to update the user's username in the same Firestore document. I do it instantly after the createUserWithEmailAndPassword() like this:
DocumentReference dDocRef = FirebaseFirestore.getInstance().document(documentPath);
Map<String, Object> updateUsernameAndPhone = new HashMap<>();
updateUsernameAndPhone.put("username", username);
updateUsernameAndPhone.put("phoneData", phoneModel);
dDocRef.update(updateUsernameAndPhone).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Toast.makeText(getApplicationContext(), "Data successfully stored in Firestore", Toast.LENGTH_SHORT).show();
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(getApplicationContext(), e.toString(), Toast.LENGTH_SHORT).show();
}
});
Now, it depends on who acts first, the cloud function or the user's device.
If the cloud functions act first, then there is no issue. The username and phoneModel both get updated into the document successfully. No issues.
But incase, the phone acts first then I get the following error:
As this error has occurred, username isn't in the document and only email and timeCreated are in the document updated by the cloud function which got late to create document so that user's device can update the username with ease.
I CAN'T use .SET instead of .update() in my app because if I use .set() and the cloud functions create the email and timeCreated fields first. Then the device will DELETE them and put username and phoneModel.
So how can I do this?
I can forcefully delay updating the username by putting it in the next activity so that cloud functions get enough time to do their job, but my signUpActivity asks for username along with email and password edit texts. I don't want to create a separate activity for that.
I used to use .update() when my data was stored in realtime database and it used to create the child even if the path didn't exist. But it looks firestore won't update if the field doesn't exist.
Any solution for this?
I tried as per #DougStevenson said and here's my code:
final String newUserUID = Objects.requireNonNull(signUpAuth.getCurrentUser()).getUid();
final String documentPath = "Agent/" + newUserUID;
FirebaseFirestore fFirestoreRef = FirebaseFirestore.getInstance();
final DocumentReference dDocRef = fFirestoreRef.document(documentPath);
fFirestoreRef.runTransaction(new Transaction.Function<Void>() {
#Nullable
#Override
public Void apply(#NonNull Transaction transaction) throws
FirebaseFirestoreException {
DocumentSnapshot documentSnapshot = transaction.get(dDocRef);
transaction.update(dDocRef, "username", username);
transaction.update(dDocRef, "phoneData", phoneModel);
return null;
}
}).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Toast.makeText(getApplicationContext(), "Data updated in Firestore . . .", Toast.LENGTH_SHORT).show();
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(getApplicationContext(), e.toString(), Toast.LENGTH_SHORT).show();
}
});
But no luck. It still gives me error: Cannot update a document which does not exist.
Use a transaction to resolve conflicts from multiple clients that are all trying to modify the same document. The transaction handler will be retried on the client if it detects the document was modified before the change could take place.

Firestore: Why is my code not calling "if (!document.exists())"?

My intention here is to display a Toast message if the user inputs an email/password combination that does not match any of the user profiles in the Firestore database.
I've been trying this multiple ways, but it refuses to call the code within "if (!document.exists()))." I've tried omitting "(!document.exists) and just using "else" - nothing.
Everything else works great. "if (document.exists())" happily returns the info and logs in. Please let me know if I need to include more info!
FirestoreRepository:
public void queryUserByEmailPassword(String email, String password) {
Query userQuery = userColRef.whereEqualTo("email", email).whereEqualTo("password", password);
userQuery.get().addOnCompleteListener(querySnapshotOnCompleteListener);
}
LoginViewModel:
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot doc : task.getResult()) {
if (!document.exists()) { // this code will NOT execute
Log.d(TAG, "Error getting documents: ", task.getException());
return;
} else { // this code executes flawlessly
user = doc.toObject(User.class);
currentUser.setValue(user);
doesUserExist.setValue(true);
repo.signIn(user.getEmail(), user.getPassword());
}
}
}
}
It looks like you're assuming that an error with the query will result in a document to show up in a QueryDocumentSnapshot. That's not the way it works. If there's an error with the query, then task.isSuccessful() will return false. You're currently not checking that case.
If you query returns no documents, then your for loop will not execute at all. It is not considered an "error" to get zero documents. If you need to know if there are no documents in the result, you should check that the QuerySnapshot contains no documents:
if (task.isSuccessful()) {
QuerySnapshot qs = task.getResult();
if (qs.size() == 0) {
// the query returned no documents - decide what to do
}
else {
// iterate the documents here
for (QueryDocumentSnapshot snapshot : qs) {
}
}
}
else {
// there was an error, use task.getException() to figure out what happened
}

Cloud Firestore: retrieving cached data via direct get?

Retrieving data from the server may take some seconds. Is there any way to retrieve cached data in the meantime, using a direct get?
The onComplete seems to be called only when the data is retrieved from the server:
db.collection("cities").whereEqualTo("state", "CA").get()
.addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
...
}
}
});
Is there any callback for the cached data?
Now it is possible to load data only from cached version. From docs
You can specify the source option in a get() call to change the default behavior.....you can fetch from only the offline cache.
If it fails, then you can again try for the online version.
Example:
DocumentReference docRef = db.collection("cities").document("SF");
// Source can be CACHE, SERVER, or DEFAULT.
Source source = Source.CACHE;
// Get the document, forcing the SDK to use the offline cache
docRef.get(source).addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
// Document found in the offline cache
DocumentSnapshot document = task.getResult();
Log.d(TAG, "Cached document data: " + document.getData());
} else {
Log.d(TAG, "Cached get failed: ", task.getException());
//try again with online version
}
}
});
I just ran a few tests in an Android app to see how this works.
The code you need is the same, no matter if you're getting data from the cache or from the network:
db.collection("translations").document("rPpciqsXjAzjpComjd5j").get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
DocumentSnapshot snapshot = task.getResult();
System.out.println("isFromCache: "+snapshot.getMetadata().isFromCache());
}
});
When I'm online this prints:
isFromCache: false
When I go offline, it prints:
isFromCache: true
There is no way to force retrieval from the cache while you're connected to the server.
If instead I use a listener:
db.collection("translations").document("rPpciqsXjAzjpComjd5j").addSnapshotListener(new DocumentListenOptions().includeMetadataChanges(), new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(DocumentSnapshot snapshot, FirebaseFirestoreException e) {
System.out.println("listen.isFromCache: "+snapshot.getMetadata().isFromCache());
}
}
);
I get two prints when I'm online:
isFromCache: true
isFromCache: false
You can disable network access and run the query to access data from cache .
For firestore :
https://firebase.google.com/docs/firestore/manage-data/enable-offline#disable_and_enable_network_access
For firebase database call db.goOffline() and db.goOnline()

Categories

Resources