前言
本来我想写个协程三部曲,但是查了下貌似协程x的api和协程基础讲的比较多了,但是实战讲的很少,或者讲实战也只是怎么用别人封装好的三方库对应的支持(retrofit,ViewModel,room等),这种还是只能算是对api的应用,如果让自己写一套,也是比较困难,于是直接写实战了
我觉得比较好的讲协程原理和协程基础api的文章(协程x的api可以看官网或搜博客),也推荐看<<深入理解kotlin协程>> 嘿嘿:
破解 Kotlin 协程(2) - 协程启动篇_kotlin 协程 thread start-CSDN博客
Kotlin Jetpack 实战 | 09. 图解协程原理 - 掘金
ps:下面所说的协程专指Kotlin协程
pps:本篇文章针对有协程基础api和协程x api有使用经验的童鞋
ppps:有人说kt协程就是个线程切换框架(并且很多博客甚至也是这样写的?),但只能说ta并没有领悟到协程的真谛:抛开其实现,协程是给方法在原有的功能(call调用,return返回)上增加了suspend挂起和resume恢复两个操作,而挂起和恢复的能力也给协程提供了异步转同步的能力,而总有人说协程是个线程切换框架,只不过是用了协程的拦截器附加的一部分功能,其核心能力还是函数挂起和恢复
正文
封装功能
需求1:封装一下动态权限请求
假设你有一套动态申请权限的api如下(可能是你用的三方的或者自己写的,我这个是自己写的,不过和框架耦合度比较高,就不发出来了)
//安卓申请动态权限,并以回调的形式返回结果
fun requestPermission(activity: Activity, mCallBack: OnRequestPermissionCallBack, vararg permissions: String){
...
}
interface OnRequestPermissionCallBack {
fun onPermissionOk()//权限全部申请成功
fun onPermissionError(permission: String)//有权限申请失败
fun onPermissionNotAsking(permission: String)//申请的权限有的被"不在询问"了
}
然后你就可以使用协程异步转同步的形式改造如下:
/**
* 在协程中进行权限申请,如果申请成功往下走,否则直接cancel,如果传入callback就使用其失败回调,否则使用默认的
*/
suspend fun aRequestPermission(activity: Activity, vararg permissions: String, mCallBack: OnRequestPermissionCallBack? = null) =
suspendCoroutine<Unit> {//挂起协程
requestPermission(activity, object : OnOkPermissionCallBack() {
override fun onPermissionOk() {
it.resume(Unit)//如果成功就恢复协程(因为kt function默认返回值是Unit,所以这里返回的是Unit)
}
override fun onPermissionError(permission: String) {
if (mCallBack == null)
super.onPermissionError(permission)
else
mCallBack.onPermissionError(permission)
it.context.cancel()//如果没申请成功就取消当前协程(当然你也可以返回另一种返回值表示失败,这样就可以判断该方法的返回值来确定是否成功)
}
override fun onPermissionNotAsking(permission: String) {
if (mCallBack == null)
super.onPermissionNotAsking(permission)
else
mCallBack.onPermissionNotAsking(permission)
it.context.cancel()
}
}, *permissions)
}
使用起来其实也比较简单,使用如下所示:
需求2:封装网络请求
如果你用的是retrofit,其已经封装了对suspend的支持,直接声明接口中的方法为suspend,返回值为对应的类即可,如下:
但是如果你觉得还需要在封装一下,或是你直接用的OkHttp或其他网络请求方案,就可以在封装一下了,如:
/**
* 检查协程网络请求的返回值和当前状态是否符合要求
* 如果不符合要求,会回调失败的接口,并返回null,不会取消当前协程
*/
suspend fun <T : Any> Call<NetBean<T>>.getOrNull(errorListener: ((data: String, msg: String, e: Throwable?) -> Unit)? = null): T? =
withIO {
try {
this@getOrNull.xxx().data//在这里获取并返回网络的数据,这个是同步的,如果是异步就在套一层挂起函数
} catch (e: Exception) {
//处理一下异常,然后在上面处理一下服务器的耦合逻辑判断
checkCoroutineState()
errorListener?.invoke("", "", e)//如果注册了请求失败的回调,就调用一下回调
null//返回null
}
}
/**
* 检查当前协程的状态,如果是被取消了就会自动抛出取消异常
*/
@OptIn(InternalCoroutinesApi::class)
suspend fun checkCoroutineState() {
val job = coroutineContext[Job] ?: return
if (!job.isActive) throw job.getCancellationException()
}
这个方法是如果请求出异常或者失败,不会取消协程,只是返回了null,也可以在封装一下,如果请求失败直接取消协程:
/**
* 检查协程网络请求的返回值和当前状态是否符合要求
* 如果不符合要求,会回调失败的接口,并取消当前协程
*/
suspend fun <T : Any> Call<NetBean<T>>.get(errorListener: ((data: String, msg: String, e: Throwable?) -> Unit)? = null): T {
val orNull = getOrNull(errorListener)
if (orNull == null) {
checkCoroutineState()
cancel()
throw CancelException()
}
return orNull
}
这样就ok了,你如果想封装一下Dialog或者解析json,是不是也有思路了呢?
需求3:一个内容初始化很慢,别的地方可能在他还没初始化的时候就要使用,此时获取处要挂起,如果内容已经初始化过了,那就直接返回,并且只有最后一次挂起的地方能拿到初始化完成的数据(这里是为了简化,否则可以使用List<Listener>,其实是我有这方面的需求)
首先我们定义结构,很简单,就一个属性和get set
class AwaitValueNotify<T : Any> {
private var t: T? = null
fun setValue(t: T) {
this.t = t
}
suspend fun aGetValue(): T = t// 我这边定义suspend方法对应于普通方法前面会加一个a,表示await
}
然后我们分析一下,我们在get的时候挂起,并把回调存在bean里,然后在set的时候调用回调即可,其中如果重复get就覆盖之前的回调,思路很简单,代码如下
import kotlinx.coroutines.cancel
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class AwaitValueNotify<T : Any> {
private var t: T? = null
private var listener: Continuation<T>? = null //协程体,其实就是用于恢复协程的回调
fun setValue(t: T) {
this.t = t
if (listener != null) {
listener?.resume(t) //恢复协程,并置空
listener = null
}
}
suspend fun aGetValue(): T = suspendCoroutine { coroutine -> //挂起协程,其协程就是调用这个suspend方法的协程
val t = t
if (t != null)
coroutine.resume(t) //如果有数据就直接恢复协程
else {
listener?.context?.cancel() //如果之前有挂起的协程,就直接取消并置空(达到同时只有一个协程能拿到数据)
listener = coroutine
}
}
fun clearListener() { //如果需要提前回收该类,需要清除并cancel其内的协程
listener?.context?.cancel()
listener = null
}
}
代码比较简单,注释写的应该可以看明白
ps:这里为演示协程只考虑了单线程的情况,所以如果需要应对多线程的话,需要自行解决并发问题
4.挂起等到Dialog点确定后恢复,点取消或被取消则取消协程
封装后的api:
/**
* 将Dialog转成suspend的
*/
suspend fun <T> awaitDialog(run: (ok: (T) -> Unit) -> Dialog?): T =
suspendCancellableCoroutine { continuation ->
var dialog: Dialog? = null
dialog = run {
continuation.resume(it)
dialog?.setOnDismissListener(null)
dialog?.dismiss()
}
if (dialog == null) {
continuation.cancel()
return@suspendCancellableCoroutine
}
continuation.invokeOnCancellation {
dialog.setOnDismissListener(null)
}
dialog.setOnDismissListener {
continuation.cancel()
}
}
可以再封装一下,比如tips弹窗,统一样式弹窗等,下面是实战中用到的
private suspend fun showDialog2(): Unit = DialogUtilKt.awaitDialog { ok ->
DialogUtil.showCenterDialog4(this, R.layout.xxx1) { v, d ->
业务逻辑...
d.tvDialogOk_2.click {
ok(Unit)
}
}
}
private suspend fun showDialog1(): Unit = DialogUtilKt.awaitDialog { ok ->
DialogUtil.showCenterDialog4(this, R.layout.xxx2) { v, d ->
业务逻辑...
d.tvDialogOk.click {
ok(Unit)
}
}
}
使用方式:先弹一个弹窗让用户确认,如果确认了第一个就弹第二个,接着调用接口和加载窗,最后接口调用成功弹toast并关闭页面
main {
showDialog1()//挂起函数
showDialog2()//挂起函数
iPost.xxx(xxx).checkAndGet()//挂起函数,该方法可以自动显示隐藏加载窗
R.string.xxx.showToast()
finish()
}
写业务逻辑
理想情况下写一个斗地主游戏
使用协程的特性,我们可以写出下面的伪代码
suspend fun main(){
while(true){
开始游戏()
}
}
suspend fun 开始游戏(){
val userList = 匹配玩家()
洗牌(userList)
val 地主 = 叫地主(userList)
出牌阶段(地主 ,userList)
结算分数(userList)
}
suspend fun 匹配玩家():List<User> = ...//从服务器获取
suspend fun 出牌阶段(地主:User, userList:List<User>){
while(userList.都有牌()){
地主.出牌()
xx.出牌()
xx.出牌()
}
}
...
实际项目中的应用
这是我用在项目中的一个功能,是从本地扫描音乐文件并做动画,然后处理上传音乐等的逻辑(请忽略有的地方Music写成了Image)
用协程写出来几乎没有嵌套,并且逻辑相对于回调清晰的多,而且如果用回调的话,就会变成回调地狱
至于Flow和Channel,我平时RxJava和基于数据驱动用的不多...以后有机会在聊
其实我觉得在服务端的事件驱动型框架中,可以更好的发挥协程的作用,以更少的资源处理更多的服务
如果有好的想法我在补充(或者童鞋们可以发评论)
end
对Kotlin或KMP感兴趣的同学可以进Q群 101786950
如果这篇文章对您有帮助的话
可以扫码请我喝瓶饮料或咖啡(如果对什么比较感兴趣可以在备注里写出来)