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

54 篇文章 2 订阅
7 篇文章 0 订阅

前言

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

我觉得比较好的讲协程原理和协程基础api的文章(协程x的api可以看官网或搜博客),也推荐看<<深入理解kotlin协程>> 嘿嘿:

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

如果这篇文章对您有帮助的话

可以扫码请我喝瓶饮料或咖啡(如果对什么比较感兴趣可以在备注里写出来)

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值