FireStore not return all results - android

I want to get data from firestore, pretend that I have 10 documents in a collection. after that I must get data inside every document, so I save data in ArrayList. But FireBase never return all documents in a collection. Sometimes it returns only 5 ,6 docs in collection that has 10 docs.
my fireBaseUtil :
fun getDocumentByQueryAList( idQuery: List<String>, callBack: (ArrayList<DocumentSnapshot>) -> Unit) {
val listDocumentSnapshot = ArrayList<DocumentSnapshot>()
collRef = fireStore.collection("myCollection")
val size = idQuery.size
for (i in 0 until size) {
val query = collRef.whereEqualTo("fieldQuery", idQuery[i])
query.get().addOnSuccessListener { documents ->
for (document in documents) {
listDocumentSnapshot.add(document)
if (i == size - 1) {
callBack.invoke(listDocumentSnapshot)
}
}
}
}
}
I log out when size = 10 , but i = 8 it called invoke....
UserRepository:
FireBaseUtil.getDocumentByQueryAList{
// myList.addAll(listGettedByCallback)
}
->> when I want to have data in my list I call FireBaseUtil.getDocumentByQueryAList. I know firebase return value async but I dont know how to get all my doc then receiver.invoke("callbackValue").
Please tell me is there any solution. Thank in advance.

The problem you are experiencing is that you are expecting the queries to be run in order like so:
get idQuery[0], then add to list, then
get idQuery[1], then add to list, then
get idQuery[2], then add to list, then
...
get idQuery[8], then add to list, then
get idQuery[9], then add to list, then
invoke callback
But in reality, all of the following things happen in parallel.
get idQuery[0] (add to list when finished)
get idQuery[1] (add to list when finished)
get idQuery[2] (add to list when finished)
...
get idQuery[8] (add to list when finished)
get idQuery[9] (add to list and invoke callback when finished)
If the get idQuery[9] finishes before some of the others, you will be invoking your callback before the list is completely filled.
A primitive way to fix this would be to count the number of finished get queries, and when all of them finish, then invoke the callback.
fun getDocumentByQueryAList( idQuery: List<String>, callBack: (ArrayList<DocumentSnapshot>) -> Unit) {
val listDocumentSnapshot = ArrayList<DocumentSnapshot>()
collRef = fireStore.collection("myCollection")
val size = idQuery.size
val finishedCount = 0
for (i in 0 until size) {
val query = collRef.whereEqualTo("fieldQuery", idQuery[i])
query.get().addOnSuccessListener { documents ->
for (document in documents) {
listDocumentSnapshot.add(document)
}
if (++finishedCount == size) { // ++finishedCount will add 1 to finishedCount, and return the new value
// all tasks done
callBack.invoke(listDocumentSnapshot)
}
}
}
}
However, this will run into issues where the callback is never invoked if any of the queries fail. You could use a addOnFailureListener or addOnCompleteListener to handle these failed tasks.
The more correct and proper way to do what you are expecting is to make use of Tasks.whenAll, which is used in a similar fashion to how you see JavaScript answers using Promise.all. I'm still new to Kotlin syntax myself, so expect the following block to potentially throw errors.
fun getDocumentByQueryAList( idQueryList: List<String>, callBack: (ArrayList<DocumentSnapshot>) -> Unit) {
val listDocumentSnapshot = ArrayList<DocumentSnapshot>()
collRef = fireStore.collection("myCollection")
val getTasks = new ArrayList<Task<Void>>();
for (idQuery in idQueryList) {
val query = collRef.whereEqualTo("fieldQuery", idQuery)
getTasks.add(
query.get()
.onSuccessTask { documents ->
// if query succeeds, add matching documents to list
for (document in documents) {
listDocumentSnapshot.add(document)
}
}
)
}
Tasks.whenAll(getTasks)
.addOnSuccessListener { results ->
callback.invoke(listDocumentSnapshot)
}
.addOnFailureListener { errors ->
// one or more get queries failed
// do something
}
}
Instead of using the callback, you could return a Task instead, where the last bit would be:
return Tasks.whenAll(getTasks)
.onSuccessTask { results ->
return listDocumentSnapshot
}
This would allow you to use the following along with other Task and Tasks methods.
getDocumentByQueryAList(idQueryList)
.addOnSuccessListener { /* ... */ }
.addOnFailureListener { /* ... */ }

Use smth like this using RxJava:
override fun getAllDocs(): Single<List<MyClass>> {
return Single.create { emitter ->
db.collection("myCollection").get()
.addOnSuccessListener { documents ->
val list = mutableListOf<MyClass>()
documents.forEach { document ->
list.add(mapDocumentToMyClass(document))}
emitter.onSuccess(list)
}
.addOnFailureListener { exception ->
emitter.onError(exception)
}
}
}
private fun mapDocumentToMyClass(documentSnapshot: QueryDocumentSnapshot) =
MyClass(
documentSnapshot.get(ID).toString(),
documentSnapshot.get(SMTH).toString(),
// some extra fields
null
)
Or smth like this using coroutine:
override suspend fun getAllDocs(): List<MyClass> {
return try {
val snapshot =
db.collection("myCollection")
.get()
.await()
snapshot.map {
mapDocumentToMyClass(it)
}
} catch (e: Exception) {
throw e
}
}
This functions helps you to get all data from one doc

Related

Wait for firestore collection.get() onSuccessListener [duplicate]

I'm building an app for a friend and I use Firestore. What I want is to display a list of favorite places but for some reason, the list is always empty.
I cannot get the data from Firestore. This is my code:
fun getListOfPlaces() : List<String> {
val places = ArrayList<String>()
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
for (document in task.result) {
val name = document.data["name"].toString()
places.add(name)
}
}
}
return list;
}
If I try to print, let's say the size of the list in onCreate function, the size is always 0.
Log.d("TAG", getListOfPlaces().size().toString()); // Is 0 !!!
I can confirm Firebase is successfully installed.
What am I missing?
This is a classic issue with asynchronous web APIs. You cannot return something now, that hasn't been loaded yet. With other words, you cannot simply return the places list as a result of a method because it will always be empty due the asynchronous behavior of the onComplete function. Depending on your connection speed and the state, it may take from a few hundred milliseconds to a few seconds before that data is available.
But not only Cloud Firestore loads data asynchronously, almost all of modern other web APIs do, since it may take some time to get the data. But let's take an quick example, by placing a few log statements in the code, to see more clearly what I'm talking about.
fun getListOfPlaces() : List<String> {
Log.d("TAG", "Before attaching the listener!");
val places = ArrayList<String>()
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d("TAG", "Inside onComplete function!");
for (document in task.result) {
val name = document.data["name"].toString()
places.add(name)
}
}
}
Log.d("TAG", "After attaching the listener!");
return list;
}
If we run this code will, the output in your logcat will be:
Before attaching the listener!
After attaching the listener!
Inside onComplete function!
This is probably not what you expected, but it explains precisely why your places list is empty when returning it.
The initial response for most developers is to try and "fix" this asynchronous behavior, which I personally recommend against it. Here is an excelent article written by Doug Stevenson that I'll highly recommend you to read.
A quick solve for this problem would be to use the places list only inside the onComplete function:
fun readData() {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
//Do what you need to do with your list
}
}
}
If you want to use the list outside, there is another approach. You need to create your own callback to wait for Firestore to return you the data. To achieve this, first you need to create an interface like this:
interface MyCallback {
fun onCallback(value: List<String>)
}
Then you need to create a function that is actually getting the data from the database. This method should look like this:
fun readData(myCallback : MyCallback) {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
myCallback.onCallback(list)
}
}
}
See, we don't have any return type anymore. In the end just simply call readData() function in your onCreate function and pass an instance of the MyCallback interface as an argument like this:
readData(object: MyCallback {
override fun onCallback(value: List<String>) {
Log.d("TAG", list.size.toString())
}
})
If you are using Kotlin, please check the other answer.
Nowadays, Kotlin provides a simpler way to achieve the same result as in the case of using a callback. This answer is going to explain how to use Kotlin Coroutines. In order to make it work, we need to add the following dependency in our build.gradle file:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1"
This library that we use is called Module kotlinx-coroutines-play-services and is used for the exact same purpose. As we already know, there is no way we can return a list of objects as a result of a method because get() returns immediately, while the callback from the Task it returns will be called sometime later. That's the reason why we should wait until the data is available.
When calling "get()" on the Task object that is returned, we can attach a listener so we can get the result of our query. What we need to do now is to convert this into something that is working with Kotlin Coroutines. For that, we need to create a suspend function that looks like this:
private suspend fun getListOfPlaces(): List<DocumentSnapshot> {
val snapshot = placesRef.get().await()
return snapshot.documents
}
As you can see, we have now an extension function called await() that will interrupt the Coroutine until the data from the database is available and then return it. Now we can simply call it from another suspend method like in the following lines of code:
private suspend fun getDataFromFirestore() {
try {
val listOfPlaces = getListOfPlaces()
} catch (e: Exception) {
Log.d(TAG, e.getMessage()) //Don't ignore potential errors!
}
}
The reason for having a empty list got perfectly answered by Alex Mamo above.
I just like to present the same thing without needing to add an extra interface.
In Kotlin you could just implement it like so:
fun readData(myCallback: (List<String>) -> Unit) {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
myCallback(list)
}
}
}
and then use it like so:
readData() {
Log.d("TAG", it.size.toString())
})

RxJava filtering with inside object

For start I must say I am begginer in RxJava.
Data class:
#Entity(tableName = "google_book")
data class GoogleBook (
#PrimaryKey(autoGenerate = true) val id: Int=0,
val items: ArrayList<VolumeInfo>)
data class VolumeInfo(val volumeInfo: BookInfo){
data class BookInfo(val title: String, val publisher: String, val description: String, val imageLinks: ImageLinks?)
data class ImageLinks(val smallThumbnail: String?)
}
Function which helps me save data to database:
fun searchBooks(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
bookRepository.getBooksFromApi(query)
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { x ->
x?.let { googleBook ->
searchJob?.cancel()
searchJob = viewModelScope.launch {
bookRepository.deleteGoogleBook()
bookRepository.insertGoogleBook(googleBook)
}
} ?: kotlin.run {
Log.d(TAG, "observeTasks: Error")
}
}
}
}
}
As seen I want to filter list within GoogleBook object by image parameter but It doesnt work. I cannot add filtering for data class ImageLinks so I have no Idea how can I make it right
I am asking mostly about this part:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
Thanks for reading
welcome to RxJava, you gonna love it.
As far as I can tell the issue with your filtering simply relies here:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null })
} // this returns you a new list filtered list here, but does not modify the original one
t // but you return the same data object here, it is not modified at all
}
// also consider naming it bookInfo if it is actually a bookInfo
What you should do is make a copy of your object with the filtered elements, something like this:
fun filterGoogleBookBySmallThumbNail(googleBook: GoogleBook): GoogleBook {
val filteredItems = googleBook.items.filter { it.volumeInfo.imageLinks?.smallThumbnail == null }
return googleBook.copy(items = ArrayList(filteredItems)) // now a new googleBook item is created with the filtered elements
}
// snippet to adjust then
bookRepository.getBooksFromApi(query)
.map { googleBook -> filterGoogleBookBySmallThumbNail(googleBook) }
//...
Some additional notes / suggestions I have:
I don't see you actually disposing of the subscription of the Observable.
bookRepository.getBooksFromApi(query) If this line returns an Observable, even if you cancel the job, you will be still observing that Observable. If it returns a Single then you are in luck, because after one element it is disposed.
To properly dispose, in cancellation you would have to do something like this(still i would recommend the other two rather, just wanted to note the not disposing):
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
val disposable = bookRepository.getBooksFromApi(query)
//...
.subscribe { x ->
//...
}
try {
awaitCancellation() // this actually suspends the coroutine until it is cancelled
} catch (cancellableException: CancellationException) {
disposable.dispose() // this disposes the observable subscription
// that way the coroutine stays alive as long as it's not cancelled, and at that point it actually cleans up the Rx Subscription
}
Seems wasteful that you start a new coroutine job just to do actions
If you want to go the Rx way, you could make the
bookRepository.deleteGoogleBook() and bookRepository.insertGoogleBook(googleBook) Completable, and setup the observable as:
bookRepository.getBooksFromApi(query)
//..
.flatMap {
bookRepository.deleteGoogleBook().andThen(bookRepository.insertGoogleBook(it)).andThen(Observable.just(it))
}
//..subscribeOn
.subscribe()
Seems weird you are mixing coroutine and RX this way
if you don't want to go full Rx, you may consider converting your Observable into a kotlin coroutine Flow, that would be easier to handle with coroutine cancellations and calling suspend functions.
I hope it's helpful

Can't reassign variable with data from Firestore [duplicate]

I'm building an app for a friend and I use Firestore. What I want is to display a list of favorite places but for some reason, the list is always empty.
I cannot get the data from Firestore. This is my code:
fun getListOfPlaces() : List<String> {
val places = ArrayList<String>()
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
for (document in task.result) {
val name = document.data["name"].toString()
places.add(name)
}
}
}
return list;
}
If I try to print, let's say the size of the list in onCreate function, the size is always 0.
Log.d("TAG", getListOfPlaces().size().toString()); // Is 0 !!!
I can confirm Firebase is successfully installed.
What am I missing?
This is a classic issue with asynchronous web APIs. You cannot return something now, that hasn't been loaded yet. With other words, you cannot simply return the places list as a result of a method because it will always be empty due the asynchronous behavior of the onComplete function. Depending on your connection speed and the state, it may take from a few hundred milliseconds to a few seconds before that data is available.
But not only Cloud Firestore loads data asynchronously, almost all of modern other web APIs do, since it may take some time to get the data. But let's take an quick example, by placing a few log statements in the code, to see more clearly what I'm talking about.
fun getListOfPlaces() : List<String> {
Log.d("TAG", "Before attaching the listener!");
val places = ArrayList<String>()
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d("TAG", "Inside onComplete function!");
for (document in task.result) {
val name = document.data["name"].toString()
places.add(name)
}
}
}
Log.d("TAG", "After attaching the listener!");
return list;
}
If we run this code will, the output in your logcat will be:
Before attaching the listener!
After attaching the listener!
Inside onComplete function!
This is probably not what you expected, but it explains precisely why your places list is empty when returning it.
The initial response for most developers is to try and "fix" this asynchronous behavior, which I personally recommend against it. Here is an excelent article written by Doug Stevenson that I'll highly recommend you to read.
A quick solve for this problem would be to use the places list only inside the onComplete function:
fun readData() {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
//Do what you need to do with your list
}
}
}
If you want to use the list outside, there is another approach. You need to create your own callback to wait for Firestore to return you the data. To achieve this, first you need to create an interface like this:
interface MyCallback {
fun onCallback(value: List<String>)
}
Then you need to create a function that is actually getting the data from the database. This method should look like this:
fun readData(myCallback : MyCallback) {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
myCallback.onCallback(list)
}
}
}
See, we don't have any return type anymore. In the end just simply call readData() function in your onCreate function and pass an instance of the MyCallback interface as an argument like this:
readData(object: MyCallback {
override fun onCallback(value: List<String>) {
Log.d("TAG", list.size.toString())
}
})
If you are using Kotlin, please check the other answer.
Nowadays, Kotlin provides a simpler way to achieve the same result as in the case of using a callback. This answer is going to explain how to use Kotlin Coroutines. In order to make it work, we need to add the following dependency in our build.gradle file:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1"
This library that we use is called Module kotlinx-coroutines-play-services and is used for the exact same purpose. As we already know, there is no way we can return a list of objects as a result of a method because get() returns immediately, while the callback from the Task it returns will be called sometime later. That's the reason why we should wait until the data is available.
When calling "get()" on the Task object that is returned, we can attach a listener so we can get the result of our query. What we need to do now is to convert this into something that is working with Kotlin Coroutines. For that, we need to create a suspend function that looks like this:
private suspend fun getListOfPlaces(): List<DocumentSnapshot> {
val snapshot = placesRef.get().await()
return snapshot.documents
}
As you can see, we have now an extension function called await() that will interrupt the Coroutine until the data from the database is available and then return it. Now we can simply call it from another suspend method like in the following lines of code:
private suspend fun getDataFromFirestore() {
try {
val listOfPlaces = getListOfPlaces()
} catch (e: Exception) {
Log.d(TAG, e.getMessage()) //Don't ignore potential errors!
}
}
The reason for having a empty list got perfectly answered by Alex Mamo above.
I just like to present the same thing without needing to add an extra interface.
In Kotlin you could just implement it like so:
fun readData(myCallback: (List<String>) -> Unit) {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
myCallback(list)
}
}
}
and then use it like so:
readData() {
Log.d("TAG", it.size.toString())
})

Retrieve data from Firestore(wait query to finish) [duplicate]

I'm building an app for a friend and I use Firestore. What I want is to display a list of favorite places but for some reason, the list is always empty.
I cannot get the data from Firestore. This is my code:
fun getListOfPlaces() : List<String> {
val places = ArrayList<String>()
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
for (document in task.result) {
val name = document.data["name"].toString()
places.add(name)
}
}
}
return list;
}
If I try to print, let's say the size of the list in onCreate function, the size is always 0.
Log.d("TAG", getListOfPlaces().size().toString()); // Is 0 !!!
I can confirm Firebase is successfully installed.
What am I missing?
This is a classic issue with asynchronous web APIs. You cannot return something now, that hasn't been loaded yet. With other words, you cannot simply return the places list as a result of a method because it will always be empty due the asynchronous behavior of the onComplete function. Depending on your connection speed and the state, it may take from a few hundred milliseconds to a few seconds before that data is available.
But not only Cloud Firestore loads data asynchronously, almost all of modern other web APIs do, since it may take some time to get the data. But let's take an quick example, by placing a few log statements in the code, to see more clearly what I'm talking about.
fun getListOfPlaces() : List<String> {
Log.d("TAG", "Before attaching the listener!");
val places = ArrayList<String>()
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d("TAG", "Inside onComplete function!");
for (document in task.result) {
val name = document.data["name"].toString()
places.add(name)
}
}
}
Log.d("TAG", "After attaching the listener!");
return list;
}
If we run this code will, the output in your logcat will be:
Before attaching the listener!
After attaching the listener!
Inside onComplete function!
This is probably not what you expected, but it explains precisely why your places list is empty when returning it.
The initial response for most developers is to try and "fix" this asynchronous behavior, which I personally recommend against it. Here is an excelent article written by Doug Stevenson that I'll highly recommend you to read.
A quick solve for this problem would be to use the places list only inside the onComplete function:
fun readData() {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
//Do what you need to do with your list
}
}
}
If you want to use the list outside, there is another approach. You need to create your own callback to wait for Firestore to return you the data. To achieve this, first you need to create an interface like this:
interface MyCallback {
fun onCallback(value: List<String>)
}
Then you need to create a function that is actually getting the data from the database. This method should look like this:
fun readData(myCallback : MyCallback) {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
myCallback.onCallback(list)
}
}
}
See, we don't have any return type anymore. In the end just simply call readData() function in your onCreate function and pass an instance of the MyCallback interface as an argument like this:
readData(object: MyCallback {
override fun onCallback(value: List<String>) {
Log.d("TAG", list.size.toString())
}
})
If you are using Kotlin, please check the other answer.
Nowadays, Kotlin provides a simpler way to achieve the same result as in the case of using a callback. This answer is going to explain how to use Kotlin Coroutines. In order to make it work, we need to add the following dependency in our build.gradle file:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1"
This library that we use is called Module kotlinx-coroutines-play-services and is used for the exact same purpose. As we already know, there is no way we can return a list of objects as a result of a method because get() returns immediately, while the callback from the Task it returns will be called sometime later. That's the reason why we should wait until the data is available.
When calling "get()" on the Task object that is returned, we can attach a listener so we can get the result of our query. What we need to do now is to convert this into something that is working with Kotlin Coroutines. For that, we need to create a suspend function that looks like this:
private suspend fun getListOfPlaces(): List<DocumentSnapshot> {
val snapshot = placesRef.get().await()
return snapshot.documents
}
As you can see, we have now an extension function called await() that will interrupt the Coroutine until the data from the database is available and then return it. Now we can simply call it from another suspend method like in the following lines of code:
private suspend fun getDataFromFirestore() {
try {
val listOfPlaces = getListOfPlaces()
} catch (e: Exception) {
Log.d(TAG, e.getMessage()) //Don't ignore potential errors!
}
}
The reason for having a empty list got perfectly answered by Alex Mamo above.
I just like to present the same thing without needing to add an extra interface.
In Kotlin you could just implement it like so:
fun readData(myCallback: (List<String>) -> Unit) {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
myCallback(list)
}
}
}
and then use it like so:
readData() {
Log.d("TAG", it.size.toString())
})

Firebase Firestore - Saving user bookmarks [duplicate]

I'm building an app for a friend and I use Firestore. What I want is to display a list of favorite places but for some reason, the list is always empty.
I cannot get the data from Firestore. This is my code:
fun getListOfPlaces() : List<String> {
val places = ArrayList<String>()
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
for (document in task.result) {
val name = document.data["name"].toString()
places.add(name)
}
}
}
return list;
}
If I try to print, let's say the size of the list in onCreate function, the size is always 0.
Log.d("TAG", getListOfPlaces().size().toString()); // Is 0 !!!
I can confirm Firebase is successfully installed.
What am I missing?
This is a classic issue with asynchronous web APIs. You cannot return something now, that hasn't been loaded yet. With other words, you cannot simply return the places list as a result of a method because it will always be empty due the asynchronous behavior of the onComplete function. Depending on your connection speed and the state, it may take from a few hundred milliseconds to a few seconds before that data is available.
But not only Cloud Firestore loads data asynchronously, almost all of modern other web APIs do, since it may take some time to get the data. But let's take an quick example, by placing a few log statements in the code, to see more clearly what I'm talking about.
fun getListOfPlaces() : List<String> {
Log.d("TAG", "Before attaching the listener!");
val places = ArrayList<String>()
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d("TAG", "Inside onComplete function!");
for (document in task.result) {
val name = document.data["name"].toString()
places.add(name)
}
}
}
Log.d("TAG", "After attaching the listener!");
return list;
}
If we run this code will, the output in your logcat will be:
Before attaching the listener!
After attaching the listener!
Inside onComplete function!
This is probably not what you expected, but it explains precisely why your places list is empty when returning it.
The initial response for most developers is to try and "fix" this asynchronous behavior, which I personally recommend against it. Here is an excelent article written by Doug Stevenson that I'll highly recommend you to read.
A quick solve for this problem would be to use the places list only inside the onComplete function:
fun readData() {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
//Do what you need to do with your list
}
}
}
If you want to use the list outside, there is another approach. You need to create your own callback to wait for Firestore to return you the data. To achieve this, first you need to create an interface like this:
interface MyCallback {
fun onCallback(value: List<String>)
}
Then you need to create a function that is actually getting the data from the database. This method should look like this:
fun readData(myCallback : MyCallback) {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
myCallback.onCallback(list)
}
}
}
See, we don't have any return type anymore. In the end just simply call readData() function in your onCreate function and pass an instance of the MyCallback interface as an argument like this:
readData(object: MyCallback {
override fun onCallback(value: List<String>) {
Log.d("TAG", list.size.toString())
}
})
If you are using Kotlin, please check the other answer.
Nowadays, Kotlin provides a simpler way to achieve the same result as in the case of using a callback. This answer is going to explain how to use Kotlin Coroutines. In order to make it work, we need to add the following dependency in our build.gradle file:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1"
This library that we use is called Module kotlinx-coroutines-play-services and is used for the exact same purpose. As we already know, there is no way we can return a list of objects as a result of a method because get() returns immediately, while the callback from the Task it returns will be called sometime later. That's the reason why we should wait until the data is available.
When calling "get()" on the Task object that is returned, we can attach a listener so we can get the result of our query. What we need to do now is to convert this into something that is working with Kotlin Coroutines. For that, we need to create a suspend function that looks like this:
private suspend fun getListOfPlaces(): List<DocumentSnapshot> {
val snapshot = placesRef.get().await()
return snapshot.documents
}
As you can see, we have now an extension function called await() that will interrupt the Coroutine until the data from the database is available and then return it. Now we can simply call it from another suspend method like in the following lines of code:
private suspend fun getDataFromFirestore() {
try {
val listOfPlaces = getListOfPlaces()
} catch (e: Exception) {
Log.d(TAG, e.getMessage()) //Don't ignore potential errors!
}
}
The reason for having a empty list got perfectly answered by Alex Mamo above.
I just like to present the same thing without needing to add an extra interface.
In Kotlin you could just implement it like so:
fun readData(myCallback: (List<String>) -> Unit) {
placesRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val list = ArrayList<String>()
for (document in task.result) {
val name = document.data["name"].toString()
list.add(name)
}
myCallback(list)
}
}
}
and then use it like so:
readData() {
Log.d("TAG", it.size.toString())
})

Categories

Resources