¿Cómo devolver una lista de la base de datos de Firestore como resultado de una función en Kotlin?
Estoy creando una aplicación para un amigo y uso Firestore. Lo que quiero es mostrar una lista de lugares favoritos pero por alguna razón la lista siempre está vacía.
No puedo obtener los datos de Firestore. Este es mi código:
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;
}
Si intento imprimir, digamos el tamaño de la lista en onCreate
función, el tamaño siempre es 0.
Log.d("TAG", getListOfPlaces().size().toString()); // Is 0 !!!
Puedo confirmar que Firebase se instaló correctamente. ¿Qué me estoy perdiendo?
Este es un problema clásico con las API web asincrónicas. No puedes devolver algo ahora que aún no se ha cargado. En otras palabras, no puedes simplemente devolver la places
lista como resultado de un método porque siempre estará vacía debido al comportamiento asincrónico de la onComplete
función. Dependiendo de la velocidad de su conexión y del estado, pueden pasar desde unos cientos de milisegundos hasta unos segundos antes de que los datos estén disponibles.
Pero no solo Cloud Firestore carga datos de forma asincrónica, casi todas las demás API web modernas lo hacen, ya que puede llevar algún tiempo obtener los datos. Pero tomemos un ejemplo rápido, colocando algunas declaraciones de registro en el código, para ver más claramente de qué estoy hablando.
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;
}
Si ejecutamos este código, la salida en su logcat será:
¡Antes de adjuntar al oyente!
¡Después de adjuntar al oyente!
¡Dentro de la función onComplete!
Probablemente esto no sea lo que esperabas, pero explica precisamente por qué tu lista de lugares está vacía cuando la devuelves.
La respuesta inicial de la mayoría de los desarrolladores es intentar "arreglar" este problema asynchronous behavior
, lo cual personalmente recomiendo. Aquí hay un excelente artículo escrito por Doug Stevenson que le recomiendo encarecidamente que lea.
Una solución rápida para este problema sería utilizar la lista de lugares sólo dentro de la onComplete
función:
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
}
}
}
Si desea utilizar la lista en el exterior, existe otro método. Debes crear tu propia devolución de llamada para esperar a que Firestore te devuelva los datos. Para lograr esto, primero necesitas crear algo interface
como este:
interface MyCallback {
fun onCallback(value: List<String>)
}
Luego necesita crear una función que realmente obtenga los datos de la base de datos. Este método debería verse así:
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)
}
}
}
Mira, ya no tenemos ningún tipo de devolución. Al final, simplemente llame a readData()
la función en su onCreate
función y pase una instancia de la MyCallback
interfaz como un argumento como este:
readData(object: MyCallback {
override fun onCallback(value: List<String>) {
Log.d("TAG", list.size.toString())
}
})
Si está utilizando Kotlin, consulte la otra respuesta .
Hoy en día, Kotlin proporciona una forma más sencilla de lograr el mismo resultado que en el caso de utilizar una devolución de llamada. Esta respuesta explicará cómo usar Kotlin Coroutines . Para que funcione, necesitamos agregar la siguiente dependencia en nuestro build.gradle
archivo:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1"
Esta biblioteca que utilizamos se llama Módulo kotlinx-coroutines-play-services y se utiliza exactamente para el mismo propósito. Como ya sabemos, no hay forma de que podamos devolver una lista de objetos como resultado de un método porque get()
regresa inmediatamente, mientras que la devolución de llamada de la Tarea que devuelve se llamará en algún momento más tarde. Ésa es la razón por la que deberíamos esperar hasta que los datos estén disponibles.
Al llamar a "get()" en el Task
objeto que se devuelve, podemos adjuntar un detector para poder obtener el resultado de nuestra consulta. Lo que debemos hacer ahora es convertir esto en algo que funcione con Kotlin Coroutines. Para eso, necesitamos crear una función de suspensión similar a esta:
private suspend fun getListOfPlaces(): List<DocumentSnapshot> {
val snapshot = placesRef.get().await()
return snapshot.documents
}
Como puede ver, ahora tenemos una función de extensión llamada await()
que interrumpirá la rutina hasta que los datos de la base de datos estén disponibles y luego los devolverá. Ahora podemos simplemente llamarlo desde otro método de suspensión como en las siguientes líneas de código:
private suspend fun getDataFromFirestore() {
try {
val listOfPlaces = getListOfPlaces()
} catch (e: Exception) {
Log.d(TAG, e.getMessage()) //Don't ignore potential errors!
}
}
Alex Mamo respondió perfectamente el motivo de tener una lista vacía arriba.
Simplemente me gusta presentar lo mismo sin necesidad de agregar nada extra interface
.
En Kotlin podrías implementarlo así:
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)
}
}
}
y luego usarlo así:
readData() {
Log.d("TAG", it.size.toString())
})