如何从Android中的任何异步操作获取数据

a_l*_*ody 9 java android kotlin firebase

(免责声明:人们通过使用诸如Facebook,firebase等请求使用异步操作时询问数据是否为空/不正确会引起很多问题。我的意图是向所有开始的人都提供一个简单的答案在android中进行异步操作,因为我在搜索后无法真正找到一个示例)

我正在尝试从我的一项操作中获取数据,当我调试它时,值就在那里了,但是当我运行它们时它们总是为空,我该如何解决呢?

火力基地

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

脸书

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

a_l*_*ody 9

什么是同步/异步操作?

好吧,同步等待任务完成。在这种情况下,您的代码将“自上而下”执行。

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

要从异步操作获取值,可以在方法中定义自己的回调以使用这些值,因为它们是从这些操作返回的。

这是Java的方法

首先定义一个接口:

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

接下来,将您的方法签名更改为如下所示:

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

接下来,无论您以前想在哪里使用这些值,请添加以下行:

   callback.myResponseCallback(yourResponseObject);
Run Code Online (Sandbox Code Playgroud)

举个例子 :

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

现在,您以前调用的方法称为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. 
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

您如何为Kotlin做到这一点? (作为一个基本示例,其中您只关心单个结果)

首先将方法签名更改为以下内容:

fun foo(callback:(YourReturnType) -> Unit) {
.....
Run Code Online (Sandbox Code Playgroud)

然后,在异步操作的结果中:

firestore.collection("something").document("document").get().addOnSuccessListener { 
                    val bar = it.get("something").toString()
                    callback.invoke(bar)
                }
Run Code Online (Sandbox Code Playgroud)

然后,在以前调用方法的位置foo,现在执行此操作:

foo { 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. 
}
Run Code Online (Sandbox Code Playgroud)

如果您的foo方法以前使用了参数:

fun foo(value:SomeType, callback:(YourType) -> Unit)
Run Code Online (Sandbox Code Playgroud)

您只需将其更改为:

foo(yourValueHere) { 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. 
    }
Run Code Online (Sandbox Code Playgroud)


Ten*_*r04 5

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

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

科特林

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

Kotlin 协程

fun foo(): String? {
   var myReturnValue: String? = null
   lifecycleScope.launch { 
       myReturnValue = someApiSuspendFunction()
   }
   return myReturnValue
}
Run Code Online (Sandbox Code Playgroud)

爪哇8

private String fooValue = null;

private String foo() {
    someApi.addOnSuccessListener(result -> fooValue = result.getValue())
        .execute();
    return fooValue;
}
Run Code Online (Sandbox Code Playgroud)

爪哇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;
}
Run Code Online (Sandbox Code Playgroud)

原因是,当您将回调或侦听器传递给 API 函数时,回调代码只会在 API 完成其工作后的某个时间运行。通过将回调传递给 API 函数,您可以对工作进行排队,但当前函数(foo()在本例中)会在该工作开始之前以及回调代码运行之前立即返回。

或者在上面的协程示例中,启动的协程不太可能在启动它的函数之前完成。

调用 API 的函数无法返回回调中返回的结果(除非它是 Kotlin 协程挂起函数)。另一个答案中解释的解决方案是让您自己的函数接受回调参数并且不返回任何内容。

或者,如果您正在使用协程,则可以使您的函数挂起,而不是启动单独的协程。当您具有挂起函数时,您必须在代码中的某个位置启动协程并处理协程中的结果。通常,您会在生命周期函数(如 )onCreate()或 UI 回调(如 OnClickListener)中启动协程。


Tyl*_*r V 5

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)
}
Run Code Online (Sandbox Code Playgroud)

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

制作自己的接口/回调

创建自己的自定义接口/回调有时可以使事情看起来更清晰,但它并不能真正帮助解决如何使用回调之外的数据的核心问题 - 它只是将 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

如何正确使用自定义回调的一些示例示例 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)
}
Run Code Online (Sandbox Code Playgroud)

将程序重新排列成更小的函数

不要编写单一的线性函数,而是将工作分解为多个函数并从回调中调用它们。您不应尝试在回调中修改局部变量并在回调后返回或使用它们(例如,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)
}
Run Code Online (Sandbox Code Playgroud)

例子

作为一个具体示例,这里是一个最小的 ViewModel,它展示了如何将异步 API 包含到程序流中以获取数据、处理数据并将其显示在 Activity 或 Fragment 中。这是用 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

真正的 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")
                   }
               }
}
Run Code Online (Sandbox Code Playgroud)

随之而来的 Activity 或 Fragment 不需要了解有关这些调用的任何信息,它只需通过调用 ViewModel 上的方法来传递操作,并观察 LiveData 以在新数据可用时更新其视图。它不能假设数据在调用 后立即可用fetchData(),但使用此模式则不需要这样做。

视图层还可以执行诸如在加载数据时显示和隐藏进度条之类的操作,以便用户知道它正在后台工作。

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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")
                   }
               }
}
Run Code Online (Sandbox Code Playgroud)

(为了完整起见,还有活动 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>
Run Code Online (Sandbox Code Playgroud)