背景
异步回调的方式虽然实现了需求,但是牺牲了可读性,过多的回调让代码变得难以维护.
解决思路
rxjava->协程
Java语言开发的时候,可以借助一些第三方库如RxJava,来让减少嵌套. 但是实际落地的效果并不理想, 因为不能保证团队里面每个人都擅长并且能够接受Rx的风格.
在使用kotlin协程的时候我发现了新世界.
协程+流式设计
我的需求是实现一个订阅功能,并将功能模块化后提供给其他产品线.
外部调用
外部调用者传入一个id,sdk告诉外部购买成功还是发生异常.
内部实现
// 偷懒 构造一个Activity的扩展方法,反正最后是以Activity的方式来提供出去
fun Activity.goSubscript(productID: String, exceptionHandler: CoroutineExceptionHandler) =
CoroutineScope(Dispatchers.Main).launch(exceptionHandler) {
SubscriptHelper(this@goSubscript).run {
// 1
init()
// 2
getAvailableHistory()?.let { purchase ->
if (checkPurchase(purchase)) {
return@launch;
}
}
// 3
val skuDetails = getAvailableProducte(productID)
// 4
val purchase = subscriptProducte(this@goSubscript, skuDetails)
// 5
val isCheckedSuccess = checkPurchase(purchase)
// 6
if (!isCheckedSuccess) {
val acknowSuccess = acknowledgePurchase(purchase)
logd(content = "客户端确认${if (acknowSuccess) "成功" else "失败"} ");
}
}
}
体会了下kotlin的协程代码风格,这大概就是Kotlin的魅力吧. 那么怎么来实现上诉风格的代码呢? 这里大概需要有两个问题要解决:
- 怎么把Java异步回调转化成suspend函数.
- 怎么处理java回调哪些异常场景.
把回调转化成suspend函数
解决办法:利用通道
- 使用suspendCoroutine
// 1. 建立和GP/AppStore的连接
suspend fun init(): Boolean = try {
withTimeout(1500) {
suspendCoroutine<Boolean> { continuation ->
billingClient =
BillingClient.newBuilder(context).setListener(listenerUpdate)
.enablePendingPurchases()
.build()
.apply {
startConnection {
onBillingSetupFinishedFun {
continuation.resume(true)
}
}
};
}
}
} catch (e: TimeoutCancellationException) {
throw InitException()
}
看上面这个初始化的栗子,先构造了一个withTimeout协程代码块,然后 在suspendCoroutine代码块会生成一个continuation参数,在onBillingSetupFinishedFun函数回调的时候,我们直接使用这个参数来发送消息,结束挂起.
例如成功的时候返回
continuation.resume(true)
异常的时候可以
continuation.resumeWithException()
- 使用channel 有的情况下回调不是通过callback完成的,例如使用Handler机制完成的回调代码,怎么转化成协程.
var channel: Channel<BillingResult> = Channel();
// 利用channel发送
var listenerUpdate: PurchasesUpdatedListener = object : PurchasesUpdatedListener {
override fun onPurchasesUpdated(
billingResult: BillingResult?,
purchases: MutableList<Purchase>?
) {
CoroutineScope(Dispatchers.Main).launch {
purchases?.let {
if (it.size >= 0) {
purchase = it[0]
}
}
channel.send(billingResult!!)
channel.close()
}
}
};
// 利用channel接收
suspend fun subscriptProducte(activity: Activity, skuDetails: SkuDetails): Purchase {
billingClient.launchBillingFlow(
activity,
BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build()
)
val result = channel.receive()
when (result.responseCode) {
BillingClient.BillingResponseCode.OK -> {
return purchase!!
}
BillingClient.BillingResponseCode.USER_CANCELED -> {
throw UserCancleException()
}
else -> {
throw SubscriptProductException(result.debugMessage)
}
}
}
也就是说channel可以单独出来作为一个对象使用.
协程异常处理
上面的调用函数设计的时候,我构造了一个参数exceptionHandler,用作整个事件的异常处理.
fun Activity.goSubscript(productID: String, exceptionHandler: CoroutineExceptionHandler) =
CoroutineScope(Dispatchers.Main).launch(exceptionHandler) {
然后我定义了一些异常类型
class InitException(msg: String = "") : Exception()
class RepeateSubscription(msg: String = "") : Exception()
class NoProducteException(msg: String = "") : Exception()
class UserCancleException(msg: String = "") : Exception()
class SubscriptProductException(msg: String = "") : Exception()
class AcknowException(msg: String = "") : Exception()
然后当内部出现购买失败的回调的时候,主动抛出一个异常,把整个事件流中断,外部处理这些异常也很简单,直接定义一个exceptionHandler(是个参数是throwable的函数)对象接收.
总结起来就是:
- 通过抛出异常来中断事件流
- 异常在协程内部会向上传导
- 外部开辟一个协程任务的时候,可以给内部指定一个异常处理器
总结
- 看过一些讲协程的文章,大多都是讲一些原理和API的使用,总感觉没有吸引到让我觉得他比线程模式更好,然后自己摸索着在项目中写了一次.
大概这就是 纸上得来终觉浅,绝知此事要躬行吧.
- Android开发,为什么要用Kotlin而不是Java