协程异步同步原语_协程提示和技巧:回调。 使用异步代码的同步方式。

协程异步同步原语

In Kotlin coroutines tips and tricks series we are going to solve common real-life issues that might appear when coroutines are used in the code.

Kotlin协程提示和技巧系列中,我们将解决在代码中使用协程时可能出现的常见现实问题。

TL;DR Wrap callbacks for asynchronous code with suspendCancellableCoroutine; code examples are available at the end of each chapter.

TL; DR使用suspendCancellableCoroutine包装异步代码的回调; 每章结尾提供了代码示例。

After playing around with Kotlin coroutines there is a good chance that you have encountered the following dilemma:

在与Kotlin协程一起玩耍之后,您很有可能遇到以下难题:

  • Writing suspend function.

    编写suspend功能。

  • Calling 3rd party asynchronous code that requires callback object with methods such as onSuccess and onFailure .

    使用诸如onSuccessonFailure方法调用需要回调对象的第三方异步代码。

  • Needing results from callback to continue coroutine execution.

    需要回调的结果以继续协程执行。

You probably already see the problem here, but let’s highlight it once again — we require results from asynchronous code within a synchronous block. Let’s take a look at the real-life example in the following section.

您可能已经在这里看到了问题,但让我们再次强调一下-我们需要同步块中异步代码的结果。 让我们在以下部分中查看实际示例。

示例:使用Google Billing API消费购买的商品 (Example: Consuming purchased items using Google Billing API)

Imagine we’re selling magic potions in our app that can be bought once at a time. Each time magic potion is purchased via Google Play Billing API it needs to be consumed in order to be available for repetitive purchase.

想象一下,我们在应用程序中出售魔药,一次可以购买一次。 每次通过Google Play结算API购买魔药时,都需要消耗掉魔药才能重复购买。

Per Billing API documentation, the developer must call consumeAync() and provide an implementation of ConsumeResponseListener.

根据Billing API文档 ,开发人员必须调用consumeAync( )并提供ConsumeResponseListener的实现。

ConsumeResponseListener object handles the result of the consumption operation. You can override the onConsumeResponse() method of the ConsumeResponseListener interface, which the Google Play Billing Library calls when the operation is complete.

ConsumeResponseListener 对象处理消耗操作的结果。 您可以覆盖ConsumeResponseListener接口的onConsumeResponse()方法, 操作完成后 Google Play计费库将调用该方法

In order to notify our user that their magic potion was successfully added to the inventory, we need to check if Billing API has consumed purchased item. Let’s create an initial code draft

为了通知用户他们的魔药已成功添加到库存中,我们需要检查Billing API是否消耗了购买的物品。 让我们创建一个初始代码草案

import com.android.billingclient.*;


/**
  * Consumes provided item
  * @param billingClient initialized billing client
  * @param consumerParams consume parameters with purchase token
  * @return true if purchase is consumed, false otherwise
 **/
suspend fun consumeItem(
 billingClient: BillingClient,
 consumeParams: ConsumeParams
): Boolean{
  // Final result of consumption
  var isConsumed: Boolean = false
  // Consumption callback that will be called once Billing API finishes processing
  val consumeResponseListener = ConsumeResponseListener { billingResult: BillingResult, purchaseToken: String ->
     isConsumed = billingResult == BillingClient.BillingResponseCode.OK                                                    
  }
  // Calling Billing API to consume item
  billingClient.consumeAsync(consumeParams, consumeResponseListener)
  return isConsumed
}

Nice try, but once we run it we quickly discover that isConsumed is always false because return statement is called before consumeResponseListener has a chance to be called. Unfortunately, BillingClient doesn’t provide a synchronous way to consume item and we have to use callbacks. But there is a way to handle this — Kotlin coroutines allow to block code execution until further notice and we can use it to our advantage!

很好的尝试,但是一旦运行它,我们很快就会发现isConsumed始终为false因为return语句是在consumeResponseListener有机会被调用之前被调用的。 不幸的是, BillingClient没有提供一种消费物品的同步方式,我们必须使用回调。 但是有一种解决方法-Kotlin协程可以阻止代码执行,直到另行通知为止,我们可以利用它来发挥我们的优势!

用CancellableContinuation包装回调 (Wrapping callbacks with CancellableContinuation)

Remember that coroutine functions are marked with suspend modifier? There is a reason for that, because they are actually suspending (temporary stopping) code execution until coroutine function returns a result or throw an exception. We can apply the same logic within the coroutine function using CacellableContinuation — wrapper for the block of code that needs to be invoked before suspended function can continue. I hope that you grasped the idea behind it, so let’s take a look at the code.

还记得协程函数标有suspend修饰符吗? 这是有原因的,因为它们实际上是在暂停(临时停止)代码执行,直到协程函数返回结果或引发异常为止。 我们可以使用CacellableContinuation在coroutine函数中应用相同的逻辑,即在挂起函数可以继续之前需要调用的代码块的包装器 我希望您掌握了背后的想法,所以让我们看一下代码。

private suspend fun consumeResponseSuspendWrapper(block: (ConsumeResponseListener) -> Unit): Int {
  return suspendCancellableCoroutine<Int> { cont: CancellableContinuation<Int> ->
    block(
      ConsumeResponseListener { billingResult: BillingResult, purchaseToken: String ->
        cont.resume(billingResult.responseCode)
      } // ConsumeResponseListener 
    ) //block
  } // suspendCancellableCoroutine
}

“So… you are wrapping ConsumeResponseListener with CancellableContinuation that is invoked with suspendCancellableCoroutine that definned in consumeResponseSuspendWrapper function?”

“所以……您要用CancellableContinuation封装ConsumeResponseListener,而CancellableContinuation可以通过suspenseCancellableCoroutine调用,而该方法是在consumpResponseSuspendWrapper函数中定义的?”

Possibly confused Reader

读者可能会感到困惑

Yeah, that’s exactly what we did. We could’ve ended this article here but it would be much better if we had an understanding of why we did it this way. In order to avoid any confusion let’s break this function down to 4 pieces:

是的,那正是我们所做的。 我们可以在这里结束本文,但是如果我们了解为什么要这样做的话,那就更好了。 为了避免混淆,我们将此功能分解为4部分:

  • ConsumeResponseListener is an interface in the Billing API library that is called when the magic potion is consumed after it’s added to the user’s inventory.

    ConsumeResponseListener是Billing API库中的接口,当将魔药添加到用户的清单中后就将其消耗掉,就会调用该接口。

  • CancellableContinuation is an interface representing a continuation after a suspension point that returns value of specified type T. This type of continuation might finish its execution with the invocation of resume(result: T) or resumeWithException(e: Excpection) methods.

    CancellableContinuation是一个接口,表示在悬浮点之后返回指定类型T的值的延续。 这种类型的延续可以通过调用resume(result:T)resumeWithException(e:Excpection)方法来完成其执行

  • suspendCancellableCoroutine simply suspends coroutine until its cancellable continuation finishes execution.

    suspendCancellableCoroutine只是暂停协程,直到其可取消的延续完成执行为止。

  • consumeResponseSuspendWrapper function that eases usage by providing ConsumeResponseListener implementation

    consumeResponseSuspendWrapper函数通过提供ConsumeResponseListener实现来简化使用

Once ConsumeResponseListener receives a response from Billing API it is basically saying: “Hey, we have received a result over here, you may no longer suspend your code and resume execution with the received result”. That’s the point where we allow our CancellableContinuation to resume execution by calling this piece of code:

一旦ConsumeResponseListener收到Billing API的响应,它基本上就是在说: “嘿,我们在这里收到了一个结果,您可能不再暂停您的代码并以收到的结果继续执行” 。 这就是我们允许我们的CancellableContinuation通过调用以下代码来恢复执行的点:

cont.resume(billingResult.responseCode)

Main idea behind calling suspendCancellableCoroutine is to suspend code execution until resume or resumeWithException of passed continuation is invoked. So if cont.resume(billingResult.responseCode) wasn’t there, then our code would hang coroutine execution indefinitely. Invocation of resume method delegates passed value to the result of suspendCancellableCoroutine execution and this is exactly what we are going to receive from consumeResponseSuspendWrapper function.

调用suspendCancellableCoroutine主要思想是挂起代码执行,直到调用传递的继续的 resumeresumeWithException 。 因此,如果没有cont.resume(billingResult.responseCode) ,则我们的代码将无限期挂起协程执行。 调用resume方法代表将值传递给suspendCancellableCoroutine执行的结果,而这正是我们将从consumeResponseSuspendWrapper函数接收的consumeResponseSuspendWrapper

Concept to rememberCode execution will be suspended until resume or resumeWithException is called within suspendCancellableCoroutine block.

记住要记住的代码执行将被挂起,直到在suspendCancellableCoroutine块中调用resumeresumeWithException

Let’s get back to code! Considering all the things above here’s how we can re-write magic potion consumption

让我们回到代码! 考虑到以上所有内容,这就是我们重写魔术药水消耗的方法

import com.android.billingclient.*;


/**
  * Consumes provided item
  * @param billingClient initialized billing client
  * @param consumerParams consume parameters with purchase token
  * @return true if purchase is consumed, false otherwise
 **/
suspend fun consumeItem(
  billingClient: BillingClient,
  consumeParams: ConsumeParams
): Boolean{
  val consumeResponseCode: Int = consumeResponseSuspendWrapper { consumeResponseListener: ConsumeResponseListener ->
    billingClient.consumeAsync(consumeParams, consumeResponseListener)
  }
  return consumeResponseCode == BillingClient.BillingResponseCode.OK
}


private suspend fun consumeResponseSuspendWrapper(block: (ConsumeResponseListener) -> Unit): Int {
  return suspendCancellableCoroutine<Int> { cont: CancellableContinuation<Int> ->
    block(
      ConsumeResponseListener { billingResult: BillingResult, purchaseToken: String ->
        cont.resume(billingResult.responseCode)
      } // ConsumeResponseListener 
    ) //block
  } // suspendCancellableCoroutine
}

Simple as that. Here consumeResponseCode calls suspendCancellableCoroutine that in turn calls billingClient.consumeAsync(…) and suspends code execution until cont.resume(…) is called. Latter is called only when the billing result is available, meaning that consumeItem(…) coroutine function is executed in a sequential manner.

就那么简单。 在这里, consumeResponseCode调用suspendCancellableCoroutine ,然后依次调用billingClient.consumeAsync(…) 暂停代码执行,直到cont.resume(…) 。 仅当开票结果可用时才调用后者,这意味着consumeItem(…)协程函数以顺序方式执行。

示例:执行使用回调的Retrofit2网络调用 (Example: executing Retrofit2 network call that uses a callback)

Retrofit is a type-safe HTTP client for Android and Java. In other words — an amazing library for network-related operations that does most of the dirty job under the hood, simplifying the development process.

Retrofit是适用于Android和Java的类型安全的HTTP客户端。 换句话说,这是一个出色的网络相关操作库,可以在后台执行大部分肮脏的工作,从而简化了开发过程。

Nevertheless, developers still need to specify how to handle the response of HTTP requests, and Retrofit utilizes callbacks for that purpose. Depending on network call success onResponse or onFailure method will be executed.

尽管如此,开发人员仍然需要指定如何处理HTTP请求的响应,并且Retrofit为此目的利用了回调 。 根据网络调用成功,将执行onResponseonFailure方法。

It’s important to note that Retrofit also supports the synchronous way of executing HTTP calls by invoking execute() method, but we will stick with callbacks in terms of this article.

重要的是要注意,Retrofit还通过调用execute()方法来支持执行HTTP调用的同步方式,但是就本文而言,我们将坚持使用回调。

Let’s say we need to implement a generic network call executor class that would take Call<T> as an argument and return T from network response or null in case of error.

假设我们需要实现一个通用的网络调用执行程序类,该类将Call <T>作为参数并从网络响应返回T或在出现错误的情况下返回null。

/**
 * Executes asynchronous Retrofit call in synchronous manner
 * @param call call to be executed
 * @return object of type T or null in case any exception occurs
 */
suspend fun <T> executeSync(call: Call<T>): T? {
  return call.enqueue(someCallback)
}

We haven’t yet defined someCallback because we would like the code to be executed line-by-line and we need to once again utilize suspendCancellableCoroutine. Let’s apply our new knowledge of coroutine suspension to our advantage by creating wrapping callSuspendWrapper function that returns the nullable object of type T.

我们尚未定义someCallback因为我们希望someCallback执行代码,并且需要再次使用suspendCancellableCoroutine 。 让我们通过创建包装callSuspendWrapper函数来返回类型T的可为空的对象,从而利用我们对协程悬挂的新知识来发挥自己的优势

private suspend fun <T> callSuspendWrapper(block: (Callback<T>) -> Unit): T? {
  return suspendCancellableCoroutine<T?> { cont: CancellableContinuation<T?> ->
    block(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
          cont.resume(response.body())
      }
      override fun onFailure(call: Call<T>, t: Throwable) {
          cont.resume(null)
      }
    })
  }
}

Time to break down the code above to avoid any confusion:

是时候分解上面的代码以避免任何混乱:

  1. Callback is initialized within cancellableContinuation

    CallbackcancellableContinuation中初始化

  2. suspendCancellableCoroutine awaits for cont.resume(...) to be invoked and returns value passed to resume(...) function

    suspendCancellableCoroutine等待cont.resume(...)被调用并返回传递给resume(...)函数的值

  3. Initialized callback object is available for use for any code that calls callSuspendWrapper

    初始化的回调对象可用于任何调用callSuspendWrapper代码

And this is the final result of executeSync implementation:

这是executeSync实现的最终结果:

/**
 * Executes asynchronous Retrofit call in synchronous manner
 * @param call call to be executed
 * @return object of type T or null in case any exception occurs
 */
suspend fun <T> executeSync(call: Call<T>): T? {
  return callSuspendWrapper {callback: Callback<T> ->
    call.enqueue(callback)
  }
}


private suspend fun <T> callSuspendWrapper(block: (Callback<T>) -> Unit): T? {
  return suspendCancellableCoroutine<T?> { cont: CancellableContinuation<T?> ->
    block(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
          cont.resume(response.body())
      }
      override fun onFailure(call: Call<T>, t: Throwable) {
          cont.resume(null)
      }
    })
  }
}

That’s it for today! We’ve taken a look at the issue that might occur in production application on two examples and successfully resolved it without any boilerplate code. Hopefully, now you have an understanding of principles behind dealing with asynchronous code in synchronous fashion using coroutines.

今天就这样! 我们通过两个示例研究了生产应用程序中可能发生的问题,并且在没有任何样板代码的情况下成功解决了该问题。 希望您现在对使用协程以同步方式处理异步代码的原理有所了解。

Alex

亚历克斯

翻译自: https://medium.com/swlh/coroutines-tips-and-tricks-callbacks-synchronous-way-to-work-with-asynchronous-code-b9fb840fb793

协程异步同步原语

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值