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

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)中啟動協程。

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>

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)
添加回答
舉報