Kotlin协程在项目中的实际应用

前言

本来我想写个协程三部曲,但是查了下貌似协程x的api和协程基础讲的比较多了,但是实战讲的很少,或者将实战也只是怎么用别人封装好的三方库对应的支持(retrofit,ViewModel,room等),这种还是只能算是对api的应用,如果让自己写一套,也是比较困难,于是直接写实战了

ps:下面所说的协程专指Kotlin协程

pps:本篇文章针对有协程基础api和协程x api有使用经验的童鞋

ppps:有人说kt协程就是个线程切换框架(并且很多博客甚至也是这样写的?),但只能说ta并没有领悟到协程的真谛:抛开其实现,协程是给方法在原有的功能上增加了挂起和恢复两个操作,而挂起和恢复的能力也给协程提供了异步转同步的能力,而总有人说协程是个线程切换框架,只不过是用了协程的拦截器附加的一部分功能,其核心能力还是函数挂起和恢复

正文

封装功能

需求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 {
            [email protected]().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:这里为演示协程只考虑了单线程的情况,所以如果需要应对多线程的话,需要自行解决并发问题

写业务逻辑

理想情况下写一个斗地主游戏

使用协程的特性,我们可以写出下面的伪代码

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 

猜你喜欢

转载自blog.csdn.net/qq_33505109/article/details/109066392