Почему моя функция, вызывающая API, возвращает пустое или нулевое значение?

#java #android #retrofit2

Вопрос:

(Отказ от ответственности: Есть масса вопросов,которые возникают у людей, спрашивающих о том, что данные являются пустыми/неверными при использовании асинхронных операций с помощью запросов, таких как facebook, firebase и т.д. Мое намерение в этом вопросе состояло в том, чтобы дать простой ответ на эту проблему всем, кто начинает работать с асинхронными операциями в Android)

Я пытаюсь получить данные из одной из моих операций, когда я отлаживаю ее с помощью точек останова или журналов, значения есть, но когда я запускаю ее, они всегда равны нулю, как я могу это решить ?

Огневая база

 firebaseFirestore.collection("some collection").get()
            .addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
                @Override
                public void onSuccess(QuerySnapshot documentSnapshots) {
                     //I want to return these values I receive here? 
            })
 

Facebook

 GraphRequest request = GraphRequest.newGraphPathRequest(
            accessToken,
            "some path",
            new GraphRequest.Callback() {
                @Override
                public void onCompleted(GraphResponse response) {
                     //I want to return these values I receive here? 
                }
            });
    request.executeAsync();
 

И т.д.

Ответ №1:

Что такое синхронная/асинхронная операция ?

Что ж, синхронно ждет, пока задача не будет выполнена. В этой ситуации ваш код выполняется «сверху вниз».

Асинхронный выполняет задачу в фоновом режиме и может уведомить вас, когда она будет завершена.

Если вы хотите вернуть значения из асинхронной операции с помощью метода/функции, вы можете определить свои собственные обратные вызовы в своем методе/функции, чтобы использовать эти значения по мере их возврата из этих операций.

Вот как для Java

Начните с определения интерфейса :

 interface Callback {
    void myResponseCallback(YourReturnType result);//whatever your return type is: string, integer, etc.
}
 

затем измените подпись своего метода, чтобы она была такой :

 public void foo(final Callback callback) { // make your method, which was previously returning something, return void, and add in the new callback interface.
 

далее, там, где вы ранее хотели использовать эти значения, добавьте эту строку :

 callback.myResponseCallback(yourResponseObject);
 

в качестве примера :

 @Override
public void onSuccess(QuerySnapshot documentSnapshots) {
    // create your object you want to return here
    String bar = document.get("something").toString();
    callback.myResponseCallback(bar);
})
 

теперь, где вы ранее вызывали свой метод, называемый foo :

 foo(new Callback() {
        @Override
        public void myResponseCallback(YourReturnType result) {
            //here, this result parameter that comes through is your api call result to use, so use this result right here to do any operation you previously wanted to do. 
        }
    });
}
 

Как вы делаете это для Котлина ?
(в качестве основного примера, когда вас интересует только один результат)

начните с изменения подписи вашего метода на что-то вроде этого:

 fun foo(callback:(YourReturnType) -> Unit) {
.....
 

затем внутри результата вашей асинхронной операции :

 firestore.collection("something")
         .document("document").get()
         .addOnSuccessListener { 
             val bar = it.get("something").toString()
             callback(bar)
         }
 

затем, там , где вы ранее назвали бы свой метод вызываемым foo , вы теперь делаете это :

 foo() { result->
    // here, this result parameter that comes through is 
    // whatever you passed to the callback in the code aboce, 
    // so use this result right here to do any operation 
    // you previously wanted to do. 
}
// Be aware that code outside the callback here will run
// BEFORE the code above, and cannot rely on any data that may
// be set inside the callback.
 

если ваш foo метод ранее принимал параметры :

 fun foo(value:SomeType, callback:(YourType) -> Unit)
 

вы просто меняете его на :

 foo(yourValueHere) { result ->
    // here, this result parameter that comes through is 
    // whatever you passed to the callback in the code aboce, 
    // so use this result right here to do any operation 
    // you previously wanted to do. 
}
 

эти решения показывают, как вы можете создать метод/функцию для возврата значений из асинхронных операций, которые вы выполнили с помощью обратных вызовов.


Однако важно понимать, что, если вы не заинтересованы в создании метода/функции для этих:

 @Override
public void onSuccess(SomeApiObjectType someApiResult) {
    // here, this `onSuccess` callback provided by the api 
    // already has the data you're looking for (in this example, 
    // that data would be `someApiResult`).
    // you can simply add all your relevant code which would 
    // be using this result inside this block here, this will 
    // include any manipulation of data, populating adapters, etc. 
    // this is the only place where you will have access to the
    // data returned by the api call, assuming your api follows
    // this pattern
})
 

Ответ №2:

Есть определенная закономерность такого рода, которую я неоднократно видел, и я думаю, что объяснение того, что происходит, помогло бы. Шаблон-это функция/метод, который вызывает API, присваивает результат переменной в обратном вызове и возвращает эту переменную.

Следующая функция/метод всегда возвращает значение null, даже если результат API не равен нулю.

Котлин

 fun foo(): String? {
   var myReturnValue: String? = null
   someApi.addOnSuccessListener { result ->
       myReturnValue = result.value
   }.execute()
   return myReturnValue
}
 

Сопрограмма Котлина

 fun foo(): String? {
   var myReturnValue: String? = null
   lifecycleScope.launch { 
       myReturnValue = someApiSuspendFunction()
   }
   return myReturnValue
}
 

Java 8

 private String fooValue = null;

private String foo() {
    someApi.addOnSuccessListener(result -> fooValue = result.getValue())
        .execute();
    return fooValue;
}
 

Java 7

 private String fooValue = null;

private String foo() {
    someApi.addOnSuccessListener(new OnSuccessListener<String>() {
        public void onSuccess(Result<String> result) {
            fooValue = result.getValue();
        }
    }).execute();
    return fooValue;
}
 

Причина в том, что при передаче обратного вызова или прослушивателя функции API этот код обратного вызова будет запущен только в будущем, когда API завершит свою работу. Передавая обратный вызов функции API, вы ставите работу в очередь, но текущая функция ( foo() в данном случае) возвращается непосредственно перед началом этой работы и до запуска кода обратного вызова.

Или, в случае приведенного выше примера сопрограммы, запущенная сопрограмма вряд ли завершится до того, как функция, которая ее запустила.

Ваша функция, вызывающая API, не может вернуть результат, возвращаемый при обратном вызове (если только это не функция приостановки сопрограммы Kotlin). Решение, объясненное в другом ответе, состоит в том, чтобы заставить вашу собственную функцию принимать параметр обратного вызова и ничего не возвращать.

В качестве альтернативы, если вы работаете с сопрограммами, вы можете приостановить свою функцию вместо запуска отдельной сопрограммы. Когда у вас есть функции приостановки, где-то в вашем коде вы должны запустить сопрограмму и обработать результаты в ней. Как правило, вы запускаете сопрограмму в функции жизненного цикла , например onCreate() , или в обратном вызове пользовательского интерфейса, например, в OnClickListener.

Ответ №3:

Другой ответ объясняет, как использовать API на основе обратных вызовов, предоставляя аналогичный API на основе обратных вызовов во внешней функции. Однако в последнее время сопрограммы Kotlin становятся все более популярными, особенно на Android, и при их использовании для таких целей обычно не рекомендуется использовать обратные вызовы. Подход Котлина заключается в использовании вместо этого функций приостановки. Поэтому, если наше приложение уже использует сопрограммы, я предлагаю не распространять API-интерфейсы обратных вызовов из сторонних библиотек на остальной наш код, а преобразовать их в функции приостановки.

Преобразование обратных вызовов в приостановку

Давайте предположим, что у нас есть этот API обратного вызова:

 interface Service {
    fun getData(callback: Callback<String>)
}

interface Callback<in T> {
    fun onSuccess(value: T)
    fun onFailure(throwable: Throwable)
}
 

Мы можем преобразовать его в функцию приостановки с помощью функции suspendCoroutine():

 private val service: Service

suspend fun getData(): String {
    return suspendCoroutine { cont ->
        service.getData(object : Callback<String> {
            override fun onSuccess(value: String) {
                cont.resume(value)
            }

            override fun onFailure(throwable: Throwable) {
                cont.resumeWithException(throwable)
            }
        })
    }
}
 

Этот способ getData() может возвращать данные напрямую и синхронно, поэтому другие функции приостановки могут использовать его очень легко:

 suspend fun otherFunction() {
    val data = getData()
    println(data)
}
 

Обратите внимание, что нам не нужно использовать withContext(Dispatchers.IO) { ... } здесь. Мы даже можем вызывать getData() из основного потока, пока мы находимся внутри контекста сопрограммы (например, внутри Dispatchers.Main ) — основной поток не будет заблокирован.

Аннулирование

Если служба обратного вызова поддерживает отмену фоновых задач, то лучше всего отменить, когда сама вызывающая сопрограмма отменена. Давайте добавим функцию отмены в наш API обратного вызова:

 interface Service {
    fun getData(callback: Callback<String>): Task
}

interface Task {
    fun cancel();
}
 

Теперь Service.getData() возвращает Task данные, которые мы можем использовать для отмены операции. Мы можем потреблять его почти так же, как и раньше, но с небольшими изменениями:

 suspend fun getData(): String {
    return suspendCancellableCoroutine { cont ->
        val task = service.getData(object : Callback<String> {
            ...
        })

        cont.invokeOnCancellation {
            task.cancel()
        }
    }
}
 

Нам нужно только переключиться с suspendCoroutine() на suspendCancellableCoroutine() и добавить invokeOnCancellation() блок.

Пример использования дооснащения

 interface GitHubService {
    @GET("users/{user}/repos")
    fun listRepos(@Path("user") user: String): Call<List<Repo>>
}

suspend fun listRepos(user: String): List<Repo> {
    val retrofit = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .build()

    val service = retrofit.create<GitHubService>()

    return suspendCancellableCoroutine { cont ->
        val call = service.listRepos(user)

        call.enqueue(object : Callback<List<Repo>> {
            override fun onResponse(call: Call<List<Repo>>, response: Response<List<Repo>>) {
                if (response.isSuccessful) {
                    cont.resume(response.body()!!)
                } else {
                    // just an example
                    cont.resumeWithException(Exception("Received error response: ${response.message()}"))
                }
            }

            override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
                cont.resumeWithException(t)
            }
        })

        cont.invokeOnCancellation {
            call.cancel()
        }
    }
}
 

Встроенная поддержка

Прежде чем мы начнем преобразовывать обратные вызовы в функции приостановки, стоит проверить, поддерживает ли уже используемая нами библиотека функции приостановки: изначально или с некоторым расширением. Многие популярные библиотеки, такие как Retrofit или Firebase, поддерживают сопрограммы и приостанавливают функции. Обычно они либо предоставляют/обрабатывают функции приостановки напрямую, либо обеспечивают приостановленное ожидание поверх своего асинхронного объекта задачи/вызова/и т. Д. Такое ожидание очень часто называют await() .

Например, Retrofit поддерживает функции приостановки напрямую с версии 2.6.0:

 interface GitHubService {
    @GET("users/{user}/repos")
    suspend fun listRepos(@Path("user") user: String): List<Repo>
}
 

Обратите внимание , что мы не только добавили suspend , но и больше не возвращаем Call , а получаем результат напрямую. Теперь мы можем использовать его без всех этих enqueue() шаблонов:

 val repos = service.listRepos(user)
 

Ответ №4:

Другие ответы здесь хорошо описывают разницу между синхронным кодом (выполняется по порядку) и асинхронным кодом (выполняется не по порядку с использованием обратных вызовов) и показывают, как передать обратный вызов, чтобы получить асинхронный результат.

Однако это на самом деле не отвечает на обе основные причины, лежащие в основе большинства вопросов, которые задают этот вопрос, — как получать и использовать данные из этих API. Передача обратного вызова просто перемещает асинхронный вызов на новый уровень к новому методу.

Полезный дизайн для использования для асинхронных вызовов заключается в обработке асинхронного ответа отдельным методом («метод обработки») и как можно меньше в самом обратном вызове, кроме вызова метода обработки с полученным результатом. Это помогает избежать многих распространенных ошибок с асинхронными API, когда вы пытаетесь изменить локальные переменные, объявленные вне области обратного вызова, или пытаетесь вернуть вещи, измененные внутри обратного вызова.

В качестве конкретного примера приведем минимальную модель представления, показывающую, как можно включить асинхронный API в поток программы для извлечения данных, их обработки и отображения в виде действия или фрагмента.

 class MainViewModel : ViewModel() {
    private val textLiveData = MutableLiveData<String>()
    val text: LiveData<String>
        get() = textLiveData

    fun fetchData() {
        // Use a coroutine here to make a dummy async call,
        // this is where you could call Firestore or other API
        // Note that this method does not _return_ the requested data!
        viewModelScope.launch {
            delay(3000)
            val t = Calendar.getInstance().time
            processData(t.toString())
        }
    }

    private fun processData(d: String) {
        // Once you get the data you may want to modify it before displaying it.
        val p = "The time is $d"
        textLiveData.postValue(p)
    }
}
 

Реальный вызов API fetchData() может выглядеть примерно так

 fun fetchData() {
    firestoreDB.collection("data")
               .document("mydoc")
               .get()
               .addOnCompleteListener { task ->
                   if (task.isSuccessful) {
                       val data = task.result.data
                       processData(data["time"])
                   }
                   else {
                       textLiveData.postValue("ERROR")
                   }
               }
}
 

Действие или Фрагмент, который сопровождается этим, не должен ничего знать об этих вызовах, он просто передает действия, вызывая методы в ViewModel, и наблюдает за живыми данными, чтобы обновить их представления, когда будут доступны новые данные. Он не может предполагать , что данные доступны сразу после вызова fetchData() , но с этим шаблоном в этом нет необходимости.

Слой представления также может показывать и скрывать индикатор выполнения во время загрузки данных, чтобы пользователь знал, что он работает в фоновом режиме.

 class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val model: MainViewModel by viewModels()

        // Observe the LiveData and when it changes, update the
        // state of the Views
        model.text.observe(this) { processedData ->
            binding.text.text = processedData 
            binding.progress.visibility = View.GONE
        }

        // When the user clicks the button, pass that action to the
        // ViewModel by calling "fetchData()"
        binding.getText.setOnClickListener {
            binding.progress.visibility = View.VISIBLE
            model.fetchData()
        }

        binding.progress.visibility = View.GONE
    }
}
 

(и, для полноты, XML-файл действия)

 <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@ id/text"
        android:layout_margin="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <Button
        android:id="@ id/get_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="Get Text"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@ id/text"
        />

    <ProgressBar
        android:id="@ id/progress"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="48dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@ id/get_text"
        />

</androidx.constraintlayout.widget.ConstraintLayout>