4 回答

TA貢獻(xiàn)1895條經(jīng)驗(yàn) 獲得超3個(gè)贊
什么是同步/異步操作?
好吧,同步等待直到任務(wù)完成。在這種情況下,您的代碼執(zhí)行“自上而下”。
異步在后臺(tái)完成一個(gè)任務(wù),并且可以在完成時(shí)通知你。
如果你想通過(guò)方法/函數(shù)從異步操作返回值,你可以在你的方法/函數(shù)中定義你自己的回調(diào)來(lái)使用這些操作返回的值。
以下是 Java 的方法
從定義接口開(kāi)始:
interface Callback {
void myResponseCallback(YourReturnType result);//whatever your return type is: string, integer, etc.
}
接下來(lái),將您的方法簽名更改為如下所示:
public void foo(final Callback callback) { // make your method, which was previously returning something, return void, and add in the new callback interface.
接下來(lái),無(wú)論你以前想使用這些值,添加這一行:
callback.myResponseCallback(yourResponseObject);
舉個(gè)例子 :
@Override
public void onSuccess(QuerySnapshot documentSnapshots) {
// create your object you want to return here
String bar = document.get("something").toString();
callback.myResponseCallback(bar);
})
現(xiàn)在,您之前調(diào)用方法的位置為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 做到這一點(diǎn)? (作為您只關(guān)心單個(gè)結(jié)果的基本示例)
首先將您的方法簽名更改為如下所示:
fun foo(callback:(YourReturnType) -> Unit) {
.....
然后,在異步操作的結(jié)果中:
firestore.collection("something")
.document("document").get()
.addOnSuccessListener {
val bar = it.get("something").toString()
callback(bar)
}
然后,在您之前調(diào)用方法 called 的地方foo,您現(xiàn)在執(zhí)行以下操作:
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方法之前接受了參數(shù):
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.
}
這些解決方案展示了如何創(chuàng)建一個(gè)方法/函數(shù)來(lái)從您通過(guò)使用回調(diào)執(zhí)行的異步操作中返回值。
但是,重要的是要理解,如果您對(duì)為這些創(chuàng)建方法/函數(shù)不感興趣:
@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貢獻(xiàn)1815條經(jīng)驗(yàn) 獲得超6個(gè)贊
我反復(fù)看到這種性質(zhì)的一種特殊模式,我認(rèn)為對(duì)正在發(fā)生的事情進(jìn)行解釋會(huì)有所幫助。該模式是調(diào)用 API 的函數(shù)/方法,將結(jié)果分配給回調(diào)中的變量,并返回該變量。
以下函數(shù)/方法始終返回 null,即使 API 的結(jié)果不為 null。
fun foo(): String? {
? ?var myReturnValue: String? = null
? ?someApi.addOnSuccessListener { result ->
? ? ? ?myReturnValue = result.value
? ?}.execute()
? ?return myReturnValue
}
Kotlin協(xié)程
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;
}
原因是,當(dāng)您將回調(diào)或偵聽(tīng)器傳遞給 API 函數(shù)時(shí),該回調(diào)代碼只會(huì)在未來(lái)某個(gè)時(shí)間運(yùn)行,當(dāng) API 完成其工作時(shí)。通過(guò)將回調(diào)傳遞給 API 函數(shù),您正在排隊(duì)工作,但當(dāng)前函數(shù)(foo()在本例中)在工作開(kāi)始之前和回調(diào)代碼運(yùn)行之前立即返回。
或者在上面的協(xié)程示例中,啟動(dòng)的協(xié)程不太可能在啟動(dòng)它的函數(shù)之前完成。
調(diào)用 API 的函數(shù)無(wú)法返回回調(diào)中返回的結(jié)果(除非它是 Kotlin 協(xié)程掛起函數(shù))。另一個(gè)答案中解釋的解決方案是讓您自己的函數(shù)采用回調(diào)參數(shù)而不返回任何內(nèi)容。
或者,如果您正在使用協(xié)程,則可以暫停函數(shù)而不是啟動(dòng)單獨(dú)的協(xié)程。當(dāng)您有暫停功能時(shí),您必須在代碼中的某處啟動(dòng)協(xié)程并在協(xié)程中處理結(jié)果。通常,您會(huì)在生命周期函數(shù)(如 )onCreate()或 UI 回調(diào)(如 OnClickListener)中啟動(dòng)協(xié)程。

TA貢獻(xiàn)1824條經(jīng)驗(yàn) 獲得超8個(gè)贊
TL;DR您傳遞給這些 API 的代碼(例如在 onSuccessListener 中)是一個(gè)回調(diào),它異步運(yùn)行(不是按照它在您的文件中寫(xiě)入的順序)。它會(huì)在以后的某個(gè)時(shí)候運(yùn)行以“回調(diào)”到您的代碼中。如果不使用協(xié)程來(lái)掛起程序,則無(wú)法“返回”在函數(shù)的回調(diào)中檢索到的數(shù)據(jù)。
什么是回調(diào)?
回調(diào)是您傳遞給某個(gè)第三方庫(kù)的一段代碼,它將在稍后發(fā)生某些事件時(shí)運(yùn)行(例如,當(dāng)它從服務(wù)器獲取數(shù)據(jù)時(shí))。重要的是要記住,回調(diào)不會(huì)按照您編寫(xiě)的順序運(yùn)行——它可能會(huì)在以后很晚的時(shí)候運(yùn)行,可能會(huì)運(yùn)行多次,或者可能根本不會(huì)運(yùn)行。下面的示例回調(diào)將運(yùn)行點(diǎn) A,啟動(dòng)服務(wù)器獲取進(jìn)程,運(yùn)行點(diǎn) C,退出函數(shù),然后在遙遠(yuǎn)的將來(lái)某個(gè)時(shí)間可能會(huì)在檢索數(shù)據(jù)時(shí)運(yùn)行點(diǎn) B。C 點(diǎn)的打印輸出始終為空。
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)
}
那么如何從回調(diào)中獲取數(shù)據(jù)呢?
制作自己的界面/回調(diào)
制作您自己的自定義界面/回調(diào)有時(shí)可以使事情看起來(lái)更清晰,但它并不能真正幫助解決如何在回調(diào)之外使用數(shù)據(jù)的核心問(wèn)題——它只是將 aysnc 調(diào)用移動(dòng)到另一個(gè)位置。如果主要 API 調(diào)用在其他地方(例如在另一個(gè)類(lèi)中),它會(huì)有所幫助。
// 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)
? ? }
}
如何正確使用自定義回調(diào)的一些示例:示例 1、示例 2、示例 3
使回調(diào)成為掛起函數(shù)
另一種選擇是使用協(xié)程將異步方法轉(zhuǎn)換為掛起函數(shù),以便它可以等待回調(diào)完成。這使您可以再次編寫(xiě)線性函數(shù)。
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)
}
將您的程序重新安排為更小的功能
與其編寫(xiě)單一的線性函數(shù),不如將工作分解為幾個(gè)函數(shù)并從回調(diào)中調(diào)用它們。您不應(yīng)嘗試在回調(diào)中修改局部變量并在回調(diào)后返回或使用它們(例如 C 點(diǎn))。當(dāng)數(shù)據(jù)來(lái)自異步 API 時(shí),您必須放棄從函數(shù)返回?cái)?shù)據(jù)的想法——如果沒(méi)有協(xié)程,這通常是不可能的。
例如,您可以在單獨(dú)的方法(一種“處理方法”)中處理異步數(shù)據(jù),并盡可能少地在回調(diào)本身中執(zhí)行操作,而不是使用接收到的結(jié)果調(diào)用處理方法。這有助于避免異步 API 的許多常見(jiàn)錯(cuò)誤,在這些錯(cuò)誤中,您嘗試修改在回調(diào)范圍外聲明的局部變量或嘗試返回從回調(diào)內(nèi)修改的內(nèi)容。當(dāng)您調(diào)用它時(shí)getResult,它開(kāi)始獲取數(shù)據(jù)的過(guò)程。當(dāng)該過(guò)程完成時(shí)(在將來(lái)的某個(gè)時(shí)間)回調(diào)調(diào)用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)
}
例子
作為一個(gè)具體示例,這里是一個(gè)最小的 ViewModel,展示了如何將異步 API 包含到程序流中以獲取數(shù)據(jù)、處理數(shù)據(jù)并將其顯示在 Activity 或 Fragment 中。這是用 Kotlin 編寫(xiě)的,但同樣適用于 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)
? ? }
}
一個(gè)真正的 API 調(diào)用fetchData()可能看起來(lái)更像這樣
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")
? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ?}
}
隨之而來(lái)的 Activity 或 Fragment 不需要知道這些調(diào)用的任何信息,它只是通過(guò)調(diào)用 ViewModel 上的方法傳遞操作,并觀察 LiveData 以在新數(shù)據(jù)可用時(shí)更新其視圖。它不能假定數(shù)據(jù)在調(diào)用 后立即可用fetchData(),但使用此模式則不需要。
視圖層還可以在加載數(shù)據(jù)時(shí)顯示和隱藏進(jìn)度條,以便用戶知道它正在后臺(tái)工作。
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 對(duì)于這種類(lèi)型的異步工作流來(lái)說(shuō)并不是絕對(duì)必要的——這里是一個(gè)如何在活動(dòng)中做同樣事情的例子
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
? ? }
}
(以及,為了完整起見(jiàn),活動(dòng) 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貢獻(xiàn)1853條經(jīng)驗(yàn) 獲得超6個(gè)贊
其他答案解釋了如何通過(guò)在外部函數(shù)中公開(kāi)類(lèi)似的基于回調(diào)的 API 來(lái)使用基于回調(diào)的 API。然而,最近 Kotlin 協(xié)程變得越來(lái)越流行,尤其是在 Android 上,并且在使用它們時(shí),通常不鼓勵(lì)為此目的使用回調(diào)。Kotlin 的方法是改用掛起函數(shù)。因此,如果我們的應(yīng)用程序已經(jīng)使用協(xié)程,我建議不要將回調(diào) API 從 3rd 方庫(kù)傳播到我們的其余代碼,而是將它們轉(zhuǎn)換為掛起函數(shù)。
將回調(diào)轉(zhuǎn)換為掛起
假設(shè)我們有這個(gè)回調(diào) API:
interface Service {
? ? fun getData(callback: Callback<String>)
}
interface Callback<in T> {
? ? fun onSuccess(value: T)
? ? fun onFailure(throwable: Throwable)
}
我們可以使用suspendCoroutine()將其轉(zhuǎn)換為掛起函數(shù):
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()可以直接同步返回?cái)?shù)據(jù),所以其他suspend函數(shù)可以很方便的使用:
suspend fun otherFunction() {
? ? val data = getData()
? ? println(data)
}
請(qǐng)注意,我們不必withContext(Dispatchers.IO) { ... }在這里使用。getData()只要我們?cè)趨f(xié)程上下文中(例如 inside ) ,我們甚至可以從主線程調(diào)用Dispatchers.Main- 主線程不會(huì)被阻塞。
取消
如果回調(diào)服務(wù)支持取消后臺(tái)任務(wù),那么最好在調(diào)用協(xié)程本身被取消時(shí)取消。讓我們?cè)诨卣{(diào) API 中添加一個(gè)取消功能:
interface Service {
? ? fun getData(callback: Callback<String>): Task
}
interface Task {
? ? fun cancel();
}
現(xiàn)在,Service.getData()返回Task我們可以用來(lái)取消操作的返回值。我們可以像以前一樣消費(fèi)它,但有一些小的變化:
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()
? ? ? ? }
? ? }
}
在我們開(kāi)始將回調(diào)轉(zhuǎn)換為掛起函數(shù)之前,有必要檢查一下我們使用的庫(kù)是否已經(jīng)支持掛起函數(shù):本機(jī)或通過(guò)一些擴(kuò)展。許多流行的庫(kù)(如 Retrofit 或 Firebase)都支持協(xié)程和掛起函數(shù)。通常,它們要么直接提供/處理掛起函數(shù),要么在異步任務(wù)/調(diào)用/等之上提供可掛起的等待。目的。這種等待經(jīng)常被命名為await()。
比如Retrofit從2.6.0開(kāi)始直接支持suspend函數(shù):
interface GitHubService {
? ? @GET("users/{user}/repos")
? ? suspend fun listRepos(@Path("user") user: String): List<Repo>
}
請(qǐng)注意,我們不僅添加了suspend,而且我們不再返回Call,而是直接返回結(jié)果?,F(xiàn)在,我們可以在沒(méi)有所有這些enqueue()樣板的情況下使用它:
val repos = service.listRepos(user)
添加回答
舉報(bào)