Как вернуть список из базы данных Firestore в результате функции в Kotlin?
Я создаю приложение для друга и использую Firestore. Я хочу отобразить список избранных мест, но по какой-то причине этот список всегда пуст.
Я не могу получить данные из Firestore. Это мой код:
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;
}
Если я попытаюсь напечатать, скажем, размер списка в функции onCreate, размер всегда будет равен 0.
Log.d("TAG", getListOfPlaces().size().toString()); // Is 0 !!!
Я могу подтвердить, что Firebase успешно установлен. Пожалуйста, дайте мне знать, если вам нужно что-то еще от меня. Что мне не хватает? Спасибо!
1 ответ
Это классическая проблема с асинхронными веб-API. Вы не можете вернуть что-то, что еще не было загружено. Другими словами, вы не можете просто вернуть places
список в результате метода, потому что он всегда будет пустым из-за асинхронного поведения onComplete
функция. В зависимости от скорости вашего соединения и состояния, эти данные могут занять от нескольких сотен миллисекунд до нескольких секунд.
Но не только Cloud Firestore загружает данные асинхронно, но почти все другие современные веб-API делают, так как получение данных может занять некоторое время. Но давайте рассмотрим небольшой пример, поместив несколько операторов журнала в коде, чтобы более четко увидеть, о чем я говорю.
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;
}
Если мы запустим этот код, результат в вашем logcat будет:
Прежде чем прикрепить слушателя!
После прикрепления слушателя!
Внутри onComplete функция!
Вероятно, это не то, что вы ожидали, но это объясняет, почему ваш список мест пуст при возврате.
Первоначальный ответ для большинства разработчиков состоит в том, чтобы попытаться "исправить" это asynchronous behavior
, который я лично рекомендую против этого. Вот отличная статья, написанная Дугом Стивенсоном, которую я настоятельно рекомендую вам прочитать.
Быстрое решение этой проблемы - использовать список мест только внутри onComplete
функция:
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
}
}
}
Если вы хотите использовать список снаружи, есть другой подход. Вам нужно создать свой собственный обратный вызов, чтобы дождаться, пока Firestore вернет вам данные. Для этого сначала нужно создать interface
как это:
interface MyCallback {
fun onCallback(value: List<String>)
}
Затем вам нужно создать функцию, которая на самом деле получает данные из базы данных. Этот метод должен выглядеть так:
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)
}
}
}
Видите, у нас больше нет возвращаемого типа. В конце просто позвоните readData()
функция в вашем onCreate
функционировать и передать экземпляр MyCallback
интерфейс в качестве аргумента, как это:
readData(object: MyCallback {
override fun onCallback(value: List<String>) {
Log.d("TAG", list.size.toString())
}
})
В настоящее время Kotlin предоставляет более простой способ достичь того же результата, что и в случае использования обратного вызова. Этот ответ объясняет, как использовать сопрограммы Котлина. Чтобы заставить его работать, нам нужно добавить следующую зависимость в нашbuild.gradle
файл:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1"
Эта библиотека, которую мы используем, называется Module kotlinx-coroutines-play-services и используется с той же целью. Как мы уже знаем, мы не можем вернуть список объектов в результате метода, потому чтоget()
возвращается немедленно, но обратный вызов от Задачи, которую он возвращает, будет вызван позже. По этой причине нам следует дождаться появления данных.
На Task
объект, который возвращается при вызове get()
, мы можем прикрепить слушателя, чтобы получить результат нашего запроса. Что нам теперь нужно сделать, так это преобразовать это во что-то, что работает с сопрограммами Kotlin. Для этого нам нужно создать функцию приостановки, которая выглядит так:
private suspend fun getListOfPlaces(): List<DocumentSnapshot> {
val snapshot = placesRef.get().await()
return snapshot.documents
}
Как видите, теперь у нас есть функция расширения под названием await()
который прервет выполнение сопрограммы до тех пор, пока не станут доступны данные из базы данных, а затем вернет их. Теперь мы можем просто вызвать его из другого метода приостановки, как в следующих строках кода:
private suspend fun getDataFromFirestore() {
try {
val listOfPlaces = getListOfPlaces()
}
catch (e: Exception) {
Log.d(TAG, e.getMessage()) //Don't ignore errors!
}
}
Алекс Мамо отлично ответил на причину пустого списка.
Я просто хотел бы представить то же самое без необходимости добавлять дополнительные interface
,
В Kotlin вы можете просто реализовать это так:
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)
}
}
}
и затем используйте это так:
readData() {
Log.d("TAG", it.size.toString())
})