I am coding an android app using Google's FireStore backend. The code to grab all the documents in a firestore collection is as follows, from the official documentation:
db.collection("cities")
.get()
.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());
}
}
});
Where the above code outputs to Log.d(...), I would like to have my program add the results of the document.getData() call to an ArrayList accessible outside the inner class/method. I'm not sure what is the best way to do this. Attempting to change the return the return type of the onComplete method yields errors. Is there a standard way of accessing elements in methods like this?
Declaring a variable and trying to mutate within the class also isn't possible, unless the variable is final, which defeats the point.
That is an asynchronous call (it launches a background process to do the Firebase query, and once that is done it executes your onComplete listener), so you can't expect to have the data in hand immediately after making the database call. For example, if your function looks like
void getData() {
final List<MyData> list = new ArrayList<>();
db.collection("cities")
.get()
.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());
list.add(new MyData(document.getData()));
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
Log.d(TAG, "List size = " + list.size()); // will print 0
// list will be empty here, the firebase call will take hundreds
// to thousands of milliseconds to complete
}
You need to structure your program so that it can wait for the data to arrive. There are a couple ways you could do this. One would be to have list be a class member that gets filled by the onComplete listener (then you have to structure the program to handle data coming in at random times).
Another way would be to have a data handler routine that takes the ArrayList and does something with it. This method could be called from the onComplete listener once you've gotten all the data. For example:
void getData() {
db.collection("cities")
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
List<MyData> list = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
Log.d(TAG, document.getId() + " => " + document.getData());
list.add(new MyData(document.getData()));
}
processData(list);
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
}
void processData(List<MyData> data) {
// do stuff with the data, or make a copy, or otherwise share it
// with the rest of your program
}
I would like to have my program add the results of the document.getData() call to an ArrayList accessible outside the inner class/method.
In this case, if you are using a model class, you should use toObject() method and add objects of type YourModelClass in an ArrayList like this:
if (task.isSuccessful()) {
List<YourModelClass> list = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
YourModelClass yourModelClass = document.toObject(YourModelClass.class);
list.add(yourModelClass);
//Do what you need to do with your list
}
}
As you can see, I advised you to use the list of YourModelClass objects inside the callback. This is because onComplete() method has an asynchronous behavior and you cannot simply use that list outside the callback because it will always be empty.
Attempting to change the return the return type of the onComplete method yields errors.
Changing the return type of the method will not give you errors but the result will always be an empty list. You cannot return something now that hasn't been loaded yet. A quick solve for this problem would be to use that list of objects only inside the onComplete() method, as I already wrote above, or if you want to use it outside, I recommend you see the last part of my anwser from this post in which I have explained how it can be done using a custom callback. You can also take a look at this video for a better understanding.
Related
I'm having a trouble with firebase queries in Studio.
I'm trying to abstract a simple query to get a list of object stored in a collection (in my case "users")
I want to create a function stored in a Class that can be called by every fragment into the project.
But I don' t find any method to do that, is to repeate the same instruction the only way to do that?
Here is an example
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
I write these lines of code every time I need them, but I want to create a method that return a List as in this example:
public static List<User> getUsers(FirebaseFirestore db) {
List<User> usersList = new ArrayList<>();
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
} else {
//error
}
return usersList;
}
Data is loaded from Firestore (and most modern cloud APIs) asynchronously, because it may take some time. Instead of blocking the app during that time, the main code continues to execute. Then when the data is available, your addOnCompleteListener callback is executed with that data.
The easiest way to see this is by adding some well-placed logging to your code:
public static List<User> getUsers(FirebaseFirestore db) {
Log.i(LOGIN, "Starting getUsers");
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
Log.i(LOGIN, "Got data");
})
Log.i(LOGIN, "Returning from getUsers");
}
When you run this code, you get the following output:
Starting getUsers
Returning from getUsers
Got data
This is probably not the order you expected, but it completely explains why the code that calls getUsers never sees the data: by the time your return usersList runs, the data hasn't loaded yet and usersList.add(document.toObject(User.class)) has never been called.
The solution is always the same: any code that needs the data from the database, must either be inside the completion callback, be called from there, or be synchronized by some other means.
A simple example is to create a custom callback function:
public interface GetUsersCallback {
void onCallback(List<User> users);
}
You then pass that to getUsers, which can then call it once it's gotten and processed the results from the database:
public static void getUsers(FirebaseFirestore db, GetUsersCallback callback) {
// 👆
List<User> usersList = new ArrayList<>();
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
callback(usersList); // 👈
}
}
And you can then use it like this:
getUsers(new GetUsersCallback() {
#Override
public void onCallback(List<User> users) {
Log.i(LOGIN, "Found "+users.size()+" users");
}
});
Asynchronous loading of data is incredibly common when dealing with cloud APIs, but it's also quite confusing when you first encounter it. I recommend reading some of these answers to learn more about it:
How to check a certain data already exists in firestore or not
How to return a DocumentSnapShot as a result of a method?
Why does my function that calls an API return an empty or null value?
Firebase Firestore get data from collection
enter image description here
/Info/Semestre1/Courses/course1 Is my full data base reference.
I'm trying to access all the courses name of all the semesters at ounce.
db.collection("Courses").get().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());
}
}
});
}
I
I got all the semesters of Info. Now how can I access all the courses names?
You are doing the same mistake which i done when i was learning more about firestore.
So the first mistake you are doing is directly accessing the "Courses" collection which
is not possible if you want to access the "Courses" collection then you must need to traverse through all the collection and document before it.
Ex :collection1/doc1/collection2/doc2/collection3
Now if you want to access the data of the collection3 then query for that is going to be like
collection1.doc().collection2.doc2().collection3.get() and then your listner thats just basic understanding before we move for you answer.
Now ans of you question
db.collection("info").document("sem-id").collection(Courses).get().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());
}
}
});
}
Let me know if still any doubt.Happy coding
I think your query in the question is actually incorrect. Based on what I see in your screenshot, to get all the semesters, you would do this:
db.collection("Info").get()
Then, to get all the courses for a specific semester in the subcollection nested under the semester, you would need a whole new query like this:
String courseId = ... // changes as you iterate the courses in Info
db.collection("Info").document(courseId).collection("Courses").get()
You can't get all the semesters and courses in one query with the database structure you have now. Firestore queries are shallow, meaning they don't consider subcollections at all. Each subcollection requires its own separate query.
How should I fetch the document fields from one collection and combine them to add a new document to another collection? I have attached picture of the database how does it looks, I want to fetch the fields from the collection show and want to update it to the new collection along with some other data:
private void savePost(String mPostTitle, String mPostContent, String mlistSpinnerC) {
final DocumentReference docRef = FirebaseFirestore.getInstance().collection("users").document(mauth.getCurrentUser().getUid());
docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document != null) {
String username = (String)
document.get("username");
String email= (String) document.get(email);
} else {
Log.d(TAG, "No such document");
}
} else {
Log.d(TAG, "get failed with ", task.getException());
}
}
});
postMap.put(Constants.POSTTTITLE, mPostTitle);
postMap.put(Constants.POSTCATEGORY, mlistSpinnerC);
postMap.put(Constants.POSTCONTENT, mPostContent);
postMap.put(Constants.TIMESTAMP, (System.currentTimeMillis()/1000));
postMap.put(Constants.USER_ID,mauth.getCurrentUser().getUid());
postMap.put("username", username);
PostsRef.document().set(postMap).addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
if(task.isSuccessful()){
Intent toHomeActivity = new Intent(AddPostActivity.this, MainActivity.class);
startActivity(toHomeActivity);
}
}
});
I am just not able to map the fields from one collection to another collection, please guide me the correct method to that.
By the time you are trying to add the username to your postMap using the following line of code:
postMap.put("username", username);
The data has not finished loading yet from the database and this is because the listener you have added to your get() call is being invoked some unknown amount of time later after your query finishes. You don't know how long it's going to take, it may take from a few hundred milliseconds to a few seconds before that data is available. The onComplete() method has an asynchronous behavior, that's why you cannot get that username in such a way.
A quick solve for this problem would be to move all that block of code related to adding data to the postMap, inside the onComplete() method. In this you are waiting for the callback and username your will be available. Otherwise I recommend you see the last part of my anwser from this post in which I have explained how it can be done using a custom callback. You can also take a look at this video for a better understanding.
My structure of Firestore database:
|
|=>root_collection
|
|=>doc1
|
|=>collection
|
|=>doc2
|
|=>collection
|
|=>doc3
|
|=>collection
Now I wanna get list of document from root_collection. There would be a list with following data {"doc1", "doc2", "doc3"}. I need it because I want to make a spinner and put these data in the spinner. Then a user would be choose some document and download it.
I try to use the code below:
firestore.collection("root_collection")
.get()
.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());
}
}
});
But the code works only then I have structure of data without collections in the documents. In other case there aren't any documents in QueryDocumentSnapshot.
Thanks!
To have a list that contains all the name of your documents within the root_collection, please use the following code:
firestore.collection("root_collection").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
List<String> list = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
list.add(document.getId());
}
Log.d(TAG, list.toString());
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
The result in your logcat will be:
[doc1, doc2, doc3]
Remember, this code will work, only if you'll have some properties within those documents, otherwise you'll end ut with an empty list.
You can call collection method to get documentin the root_collections then hold documents ID which will be used to get document's collection later.
create root collection object as:
data class RootCollection(
#DocumentId val id: String,
val sampleField: String?
) {
// empty constructor to allow deserialization
constructor(): this(null, null)
}
Then get the collection using FirebaseFirestore method collection as follows:
val querySnapshot = firestore.collection("root_collection")
.get()
.await()
if(!querySnapshot.isEmpty) {
Result.SUCCESS(querySnapshot.toObjects(RootCollection::class.java))
} else {
Result.ERROR(Exception("No data available"))
}
Then to get collection in the document call
firestore.collection("root_collection/$documentId/collection")
Note that I have use kotlin coroutines await method as describe in this link
The Result class am using to save state of returned data and handle errors.
sealed class Result<out T: Any> {
data class SUCCESS<out T: Any>(val data: T) : Result<T>()
data class ERROR(val e: Exception): Result<Nothing>()
object LOADING : Result<Nothing>()
}
Is this script wrong, because the data I receive is null while I've added data on the Cloud Firestore. I do not use RecyclerView because I only need one data only.
This is the script:
private void getCustomer(){
firestoreDB.collection("customer")
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
customers = new ArrayList<>();
for (DocumentSnapshot doc : task.getResult()) {
Customer customer = doc.toObject(Customer.class);
customer.setId_customer(doc.getId());
customers.add(customer);
}
} else {
// Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
firestoreListener = firestoreDB.collection("customer")
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
if (e != null) {
// Log.e(TAG, "Listen failed!", e);
return;
}
customers = new ArrayList<>();
for (DocumentSnapshot doc : documentSnapshots) {
Customer customer = doc.toObject(Customer.class);
customer.setId_customer(doc.getId());
customers.add(customer);
}
}
});
id_customer = customers.get(0).getId_customer();
}
and this is my firestore:
You cannot use something now that hasn't been loaded yet. With other words, you cannot simply use the following line of code:
id_customer = customers.get(0).getId_customer();
Outside the onSuccess() method because it will always be null due the asynchronous behaviour of this method. This means that by the time you are trying to use the id_customer variable outside that method, the data hasn't finished loading yet from the database and that's why is not accessible.
A quick solve for this problem would be to use that result only inside the onSuccess() method, or if you want to use it outside, I recommend you see the last part of my anwser from this post in which I have exaplined how it can be done using a custom callback. You can also take a look at this video for a better understanding.