为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

慕勒3428872 2023-05-10 17:00:15
我试图从我的一个操作中获取数据,当我使用断点或日志调试它时,值就在那里,但是当我运行它时它们总是空的,我该如何解决这个问题?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.FacebookGraphRequest 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? = nullsomeScope.launch {    result = someSuspendFunctionToRetrieveSomething()    //I want to return the value I received here... }Log.d("result", result.toString()) //...but it is still null here.ETC。
查看完整描述

4 回答

?
蛊毒传说

TA贡献1895条经验 获得超3个赞

什么是同步/异步操作?


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


异步在后台完成一个任务,并且可以在完成时通知你。


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


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

         }

然后,在您之前调用方法 called 的地方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

})


查看完整回答
反对 回复 2023-05-10
?
红糖糍粑

TA贡献1815条经验 获得超6个赞

我反复看到这种性质的一种特殊模式,我认为对正在发生的事情进行解释会有所帮助。该模式是调用 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

}

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


查看完整回答
反对 回复 2023-05-10
?
有只小跳蛙

TA贡献1824条经验 获得超8个赞

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)

}

例子

作为一个具体示例,这里是一个最小的 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)

    }

}

一个真正的 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")

                   }

               }

}

随之而来的 Activity 或 Fragment 不需要知道这些调用的任何信息,它只是通过调用 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>


查看完整回答
反对 回复 2023-05-10
?
墨色风雨

TA贡献1853条经验 获得超6个赞

其他答案解释了如何通过在外部函数中公开类似的基于回调的 API 来使用基于回调的 API。然而,最近 Kotlin 协程变得越来越流行,尤其是在 Android 上,并且在使用它们时,通常不鼓励为此目的使用回调。Kotlin 的方法是改用挂起函数。因此,如果我们的应用程序已经使用协程,我建议不要将回调 API 从 3rd 方库传播到我们的其余代码,而是将它们转换为挂起函数。


将回调转换为挂起


假设我们有这个回调 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函数可以很方便的使用:


suspend fun otherFunction() {

    val data = getData()

    println(data)

}

请注意,我们不必withContext(Dispatchers.IO) { ... }在这里使用。getData()只要我们在协程上下文中(例如 inside ) ,我们甚至可以从主线程调用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()块。


使用 Retrofit 的示例


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开始直接支持suspend函数:


interface GitHubService {

    @GET("users/{user}/repos")

    suspend fun listRepos(@Path("user") user: String): List<Repo>

}

请注意,我们不仅添加了suspend,而且我们不再返回Call,而是直接返回结果。现在,我们可以在没有所有这些enqueue()样板的情况下使用它:


val repos = service.listRepos(user)


查看完整回答
反对 回复 2023-05-10
  • 4 回答
  • 0 关注
  • 151 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信