Как вернуть список из базы данных 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 успешно установлена. Что мне не хватает?


person Jane Ashley    schedule 30.07.2018    source источник
comment
Просто проверьте это.   -  person Alex Mamo    schedule 30.07.2018
comment
Спасибо, но это для Java, я не знаю Java, я изучаю Kotlin. Сможете ли вы решить это в Котлине?   -  person Jane Ashley    schedule 30.07.2018
comment
Хорошо, напишу вам ответ на Котлине.   -  person Alex Mamo    schedule 30.07.2018


Ответы (3)


Это классическая проблема с асинхронными веб-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, проверьте другой ответ.

person Alex Mamo    schedule 30.07.2018
comment
Хорошо, держи меня в курсе. - person Alex Mamo; 30.07.2018
comment
Это потрясающе, это сработало. Спасибо за отличное объяснение !! - person Jane Ashley; 30.07.2018
comment
Я также видел видео из этого поста, на Java почти то же самое. - person Jane Ashley; 30.07.2018
comment
Спасибо @AlexMamo за отличные ответы и объяснения, это всегда очень помогает. Какой из них является лучшим способом получить результат от асинхронных API-интерфейсов, используя обратный вызов или используя поток runBlocking, а затем возвращая результат с помощью функции async. - person Deepak Kumar; 25.10.2018
comment
@deepakkumar Использование обратного вызова. - person Alex Mamo; 26.10.2018

В настоящее время Kotlin предоставляет более простой способ достичь того же результата, что и в случае использования обратного вызова. В этом ответе объясняется, как использовать Kotlin Coroutines. Чтобы заставить его работать, нам нужно добавить следующую зависимость в наш build.gradle файл:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1"

Эта библиотека, которую мы используем, называется Module kotlinx- coroutines-play-services и используется с той же целью. Как мы уже знаем, мы не можем вернуть список объектов в результате метода, потому что get() возвращается немедленно, а обратный вызов из Задачи, которую он возвращает, будет вызван позже. Вот почему нам следует подождать, пока данные станут доступны.

При вызове get () для возвращаемого объекта Task мы можем присоединить слушателя, чтобы получить результат нашего запроса. Что нам нужно сделать сейчас, так это преобразовать это во что-то, что работает с Kotlin Coroutines. Для этого нам нужно создать функцию приостановки, которая выглядит так:

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 potential errors!
    }
}
person Alex Mamo    schedule 01.12.2019
comment
Что такое placesRef? - person Rafael; 19.09.2020
comment
@Rafael Это объект CollectionReference, который точно указывает на коллекцию places. - person Alex Mamo; 19.09.2020

Причина пустого списка была прекрасно объяснена Алексом Мамо выше.

Мне просто нравится представлять то же самое, не добавляя лишнего 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())
})
person FabZbi    schedule 31.03.2019
comment
Очень простое и чистое решение. Работает как шарм. - person CEO tech4lifeapps; 06.09.2019
comment
Ошибка показывает, диапазон цикла For должен иметь метод iterator () - person Codist; 28.07.2021