为什么我的函数调用 API 或启动协程返回空值或空值



(免责声明:人们在通过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... 
});
//...and use the returned value here.

脸书

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();
//...and use the returned value here.

Kotlin 协程

var result: SomeResultType? = null
someScope.launch {
result = someSuspendFunctionToRetrieveSomething()
//I want to return the value I received here... 
}
Log.d("result", result.toString()) //...but it is still null here.

等。

什么是同步/异步操作?

好吧,同步会等到任务完成。在这种情况下,代码"自上而下"执行。

异步在后台完成任务,并可以在任务完成时通知您。

如果要通过方法/函数从异步操作返回值,可以在方法/函数中定义自己的回调,以便在从这些操作返回时使用这些值。

以下是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. 
}
});
}

你如何为 Kotlin 做到这一点?(作为一个基本的例子,你只关心一个结果)

首先将方法签名更改为如下所示的内容:

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
})

我反复看到这种性质的特殊模式,我认为对正在发生的事情的解释会有所帮助。模式是一个函数/方法,它调用 API,将结果分配给回调中的变量,并返回该变量。

以下函数/方法始终返回 null,即使 API 的结果不为 null。

科特林

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

Kotlin 协程

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

爪哇 8

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

爪哇 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())或 UI 回调(如 OnClickListener)中启动协程。

TL;DR您传递给这些 API 的代码(例如在 onSuccessListener 中)是一个回调,它以异步方式运行(而不是按照写入文件中的顺序)。它会在将来的某个时候运行,以"回调"到您的代码中。如果不使用协程挂起程序,则无法从函数"返回"在回调中检索到的数据。

什么是回调?

回调是您传递给某个第三方库的一段代码,当某些事件发生时(例如,当它从服务器获取数据时),它将在以后运行。重要的是要记住,回调不是按照您编写的顺序运行的 - 它可能会在将来很晚的时候运行,可能会运行多次,或者可能永远不会运行。下面的示例回调将运行点 A,启动服务器获取过程,运行点 C,退出函数,然后在遥远的将来某个时间可能会在检索数据时运行点 B。C 点的打印输出将始终为空。

fun getResult() {
// Point A
var r = ""
doc.get().addOnSuccessListener { result ->
// The code inside the {} here is the "callback"
// Point B - handle result
r = result // don't do this!
}
// Point C - r="" still here, point B hasn't run yet
println(r)
}

那么如何从回调中获取数据呢?

制作自己的接口/回调

制作自己的自定义接口/回调有时可以使事情看起来更干净,但它并不能真正帮助解决如何在回调之外使用数据的核心问题 - 它只是将 aysnc 调用移动到另一个位置。如果主 API 调用在其他地方(例如在另一个类中),它会有所帮助。

// you made your own callback to use in the
// async API
fun getResultImpl(callback: (String)->Unit) {
doc.get().addOnSuccessListener { result ->
callback(result)
}
}
// but if you use it like this, you still have
// the EXACT same problem as before - the printout 
// will always be empty
fun getResult() {
var r = ""
getResultImpl { result ->
// this part is STILL an async callback,
// and runs later in the future
r = result
}
println(r) // always empty here
}
// you still have to do things INSIDE the callback,
// you could move getResultImpl to another class now,
// but still have the same potential pitfalls as before
fun getResult() {
getResultImpl { result ->
println(result)
}
}

如何正确使用自定义回调的一些示例:示例 1、示例 2、示例 3

使回调成为挂起函数

另一种选择是使用协程将异步方法转换为挂起函数,以便它可以等待回调完成。这使您可以再次编写线性外观的函数。

suspend fun getResult() {
val result = suspendCoroutine { cont ->
doc.get().addOnSuccessListener { result ->
cont.resume(result)
}
}
// the first line will suspend the coroutine and wait
// until the async method returns a result. If the 
// callback could be called multiple times this may not
// be the best pattern to use
println(result)
}

将程序重新排列为较小的函数

与其编写整体线性函数,不如将工作分解为多个函数,并从回调中调用它们。您不应该尝试修改回调中的局部变量,并在回调后返回或使用它们(例如 C 点)。当函数来自异步 API 时,您必须摆脱从函数返回数据的想法 - 如果没有协程,这通常是不可能的。

例如,您可以在单独的方法("处理方法")中处理异步数据,并在回调本身中尽可能少地执行操作,而不是使用接收到的结果调用处理方法。这有助于避免异步 API 的许多常见错误,在这些错误中,您尝试修改在回调范围之外声明的局部变量或尝试返回从回调中修改的内容。当您调用getResult时,它将启动获取数据的过程。当该过程完成(将来的某个时间)时,回调调用showResult来显示它。

fun getResult() {
doc.get().addOnSuccessListener { result ->
showResult(result)
}
// don't try to show or return the result here!
}
fun showResult(result: String) {
println(result)
}

这里举一个具体的例子,展示了如何将异步 API 包含在程序流中,以获取数据、处理数据并将其显示在活动或片段中。这是用 Kotlin 编写的,但同样适用于 Java。

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)
// pretend this is a slow network call, this part
// won't run until 3000 ms later
val t = Calendar.getInstance().time
processData(t.toString())
}
// anything out here will run immediately, it will not
// wait for the "slow" code above to run first
}
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)
}
}

fetchData()中真正的 API 调用可能看起来更像这样

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 上调用方法来传递操作,并观察 LiveData 以在新数据可用时更新其视图。它不能假设数据在调用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
}
}

ViewModel 对于这种类型的异步工作流并不是绝对必需的 - 下面是如何在活动中执行相同操作的示例

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// When the user clicks the button, trigger the async
// data call
binding.getText.setOnClickListener {
binding.progress.visibility = View.VISIBLE
fetchData()
}
binding.progress.visibility = View.GONE
}

private fun fetchData() {
lifecycleScope.launch {
delay(3000)
val t = Calendar.getInstance().time
processData(t.toString())
}
}

private fun processData(d: String) {
binding.progress.visibility = View.GONE
val p = "The time is $d"
binding.text.text = p
}
}

(以及,为了完整起见,活动 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>

其他答案通过在外部函数中公开类似的基于回调的 API 来解释如何使用基于回调的 API。然而,最近 Kotlin 协程变得越来越流行,尤其是在 Android 上,在使用它们时,通常不鼓励出于此类目的使用回调。Kotlin 方法是改用挂起函数。因此,如果我们的应用程序已经使用协程,我建议不要将回调 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)

最新更新