协程学习(十)协程简单的使用之Flow学习

543 阅读5分钟

Flow 是协程中非常重要的一环,个人感觉他非常重要的原因是因为他比较好用,先来说一下Flow 的概念 Flow 是一个冷流,就是在没有监听结果前是不会生产数据的,也就是在没有 collect 之前,是没有数据产生的

先来看一下如何创建一个flow

案例1:

(1..20).asFlow().collect{
    println(it)
}

在flow 中已经为我们封装了非常多的创建方法,我们只要调用 asFlow 就可以创建出来了,上面这个案例如果想要手动实现该如何写呢

案例2:

flow {
    (1..20).forEach { 
        emit(it)
    }
}.collect{
    
}

代码也是非常的简单,我们只需要调用发射数据的方法就可以了,他的使用上与 RxJava 非常的相似的,下面先写一个错误,引出我们接下来的问题

案例3:

@JvmStatic
fun main(arge:Array<String>){

    runBlocking {
        flow{
            withContext(Dispatchers.IO){
                (1..20).forEach {
                    emit(it)
                }
            }
        }.map {
            it*it
        }.collect{
            println("result:$it")
        }
    }
}

我上面的使用方法报了一个错误,这个错误是

Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
		Flow was collected in [BlockingCoroutine{Active}@49d56b50, BlockingEventLoop@61187316],
		but emission happened in [DispatchedCoroutine{Active}@12f81b2c, Dispatchers.IO].
		Please refer to 'flow' documentation or use 'flowOn' instead

具体信息如下 image.png

先来说一下为什么会出现这个错误,已经出现了这个错误给了我们的提示是什么呢

我们在协程中讲到过 CoroutineContext 是有 Job Dispatchers CoroutineName ExceptionErrorHandler 他们四个来组成的,更新了其中一个就会导致 CoroutineContext 的变化, flow 出现这个问题的原因就是 发射数据使用了新的 CoroutineContext ,而 collect 使用的还是原始的 CoroutineContext ,也就是说 发射与收集使用的同一个 CoroutineContext, 如果他们使用同一个 CoroutineContext 会导致什么问题呢,来看下面的例子

案例4:

@JvmStatic
fun main(arge:Array<String>){

    runBlocking {
        flow{
            (1..20).forEach {
                emit(it)
                if(it==5){
                    throw NullPointerException("发射出错了")
                }
            }
        }.collect{
            println("result:$it")
        }
    }
}

在 it ==5 的时候抛出了一个异常,这个异常导致收集方再也无法继续收集了,来看一下结果

image.png

现在觉得这个事情是很正常的事情,那么如果接受方出现异常发送方的表现如何呢,案例如下

案例5:

@JvmStatic
fun main(arge:Array<String>){

    runBlocking {
        flow{
            (1..20).forEach {
                emit(it)
            }
        }.collect{
            println("result:$it")
            if(it==5){
                throw NullPointerException("发射出错了")
            }
        }
    }
}

image.png

结果还是一样的,所以在flow 的使用过程中不管是发送方还是收集方哪一方出现了问题,整个链路都会终止

可以看到我们在回到案例3 ,可以看到在 flow 中是不可以切换 Dispatchers 的,那么我们该如何切换呢

案例6:

@JvmStatic
fun main(arge: Array<String>) {
    runBlocking {
        flow {
            (1..20).forEach {
                emit(it)
                println("发送方 Thread Name:${Thread.currentThread().name}")
            }
        }.flowOn(Dispatchers.IO)
            .collect {
                println("result:$it 收集方 Thread Name:${Thread.currentThread().name}")
            }
    }
}

我们可以使用 flowOn 来切换 flowOn 上游的线程,注意是 当前flowOn上游,并不是所有的

再来看看flow 中我们比较关注的取消的问题,当我创建了一个协程,他会返回多个数据,如果返回的数据符合某些条件时取消当前flow

案例7:

@JvmStatic
fun main(arge: Array<String>) {
    runBlocking {
        (1..20).asFlow()
            .collect {
                println("result:$it")
                if (it == 5) {
                    currentCoroutineContext().cancel()
                }
            }
    }
}

结果如下 image.png

可以发现在收集方收集时如果数据是 5就取消当前flow ,但是还是所有的数据都出现了,那么他在发送时没有对flow的状态做校验,那么该如何改呢

案例8:

@JvmStatic
fun main(arge: Array<String>) {
    runBlocking {
        (1..20).asFlow()
            .cancellable()
            .collect {
                println("result:$it")
                if (it == 5) {
                    currentCoroutineContext().cancel()
                }
            }
    }
}

使用 cancellable 后每次发送数据前就会去判断flow 的状态,结果如下

image.png

在调试案例的过程中还发现,如果加入了 flowOn(Dispatchers.IO) ,也是无法停止的原因是,所有数据已经被发送到管道里面了,你停止的是没有新的数据产生,但是对于已经被添加入管道里面的数据是没有影响的,但是我们可以在 collect 处判断 CoroutineContext 的状态来决定是否使用数据

案例 9:

@JvmStatic
fun main(arge: Array<String>) {
    runBlocking {
        (1..20).asFlow()
            .cancellable()
            .flowOn(Dispatchers.IO)
            .collect {
                println("result:$it  active:${currentCoroutineContext().isActive}")
                if (it == 5) {
                    currentCoroutineContext().cancel()
                }
            }
    }
}

image.png

在没有取消的情况下,每次接收到数据这个activie 都是true ,我们可以用他来做判断

再来看看flow 的异常处理,这也是flow 使用非常方便的地方

案例10:

@JvmStatic
fun main(arge: Array<String>) {
    runBlocking {
        flow {
            (1..5).forEach {
                emit(it)
                delay(10)
                if(it==3){
                    throw NullPointerException("发送方抛异常")
                }
            }
        }.catch{
            println("上游发生了异常:${it.message}")
        }.flowOn(Dispatchers.IO)
            .collect {
                println("下游结果:$it")
            }
    }
}

结果如下图

image.png

可以看到如果在发射方发生了异常,我们可以使用 catch 方法来捕获住异常,那么下游该如何处理呢,看下面的例子

案例11:

@JvmStatic
fun main(arge: Array<String>) {
    runBlocking {
        flow {
            (1..5).forEach {
                emit(it)
                delay(10)
                if(it==3){
                    throw NullPointerException("发送方抛异常")
                }
            }
        }.catch{
            println("上游发生了异常:${it.message}")
        }.flowOn(Dispatchers.IO)
            .collect {
                println("下游结果:$it")
                if(it==2){
                    throw NullPointerException("收集方抛异常")
                }
            }
    }
}

image.png

可以看到这种问题就比较难受了,catch 只能捕获发射方的异常,无法捕获收集方的异常,这里这能使用try catch了

但是如果我们配合配合前面协程里面的异常处理

案例12:

@JvmStatic
fun main(arge: Array<String>) {
    runBlocking {
        launch(Job()+ TsmEmptyCoroutineExceptionHandler()) {
            flow {
                (1..5).forEach {
                    emit(it)
                    delay(10)
                    if(it==3){
                        throw NullPointerException("发送方抛异常")
                    }
                }
            }.catch{
                println("上游发生了异常:${it.message}")
            }.flowOn(Dispatchers.IO)
                .collect {
                    println("下游结果:$it")
                    if(it==2){
                        throw NullPointerException("收集方抛异常")
                    }
                }
        }
        delay(1000)
    }
}

image.png

TsmEmptyCoroutineExceptionHandler 就是按照官方给的例子写的,由于在调试过程中异常只需要打印,每次写又比较麻烦,就自己写了一个无参数的打印方法

@Suppress("FunctionName")
public inline fun TsmEmptyCoroutineExceptionHandler(): CoroutineExceptionHandler =
    object : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
        override fun handleException(context: CoroutineContext, exception: Throwable) {
            println("TsmEmptyCoroutineExceptionHandler :${exception.message}")
        }
    }

再来说说 flow 中比较常用的方法

案例13

@JvmStatic
fun main(arge: Array<String>) {
    runBlocking {
        (1..4).asFlow()
            .cancellable()
            .onEach {
                println("每次都会走到这里,修改数据也没用,但是可以做一些其他操作,比如说延时,")
                delay(100)
                it*it
            }.onStart {
                println("onStart...")
            }.flowOn(Dispatchers.IO)
            .onCompletion {
                println("onCompletion...正常执行完成与异常都会走到这里")
            }.map {
                println("转换数据重新发送")
                "这里会将:${it}变成字符串"
            }.catch {
                // 这里可以捕获异常
            }.collect{
                println("result:$it")
            }
    }
}

结果如下

image.png

看到flow的很多用法是不是一下子就和RxJava 联系起来了,那么我们可不可以用flow 来封装网络请求呢,答案肯定是可以的,写一个demo测试一下

案例14:

请求的简单封装

suspend fun <R> getFlowReq(req: suspend () -> R, error: (Throwable) -> Unit={}): Flow<R> {
    return flow<R> {
        emit(req())
    }.flowOn(Dispatchers.IO)//切换线程
        .cancellable()// 性能会降低一点点,但是我感觉这个很重要
        .onStart {
            // 开始Loading 弹窗
            println("start..")
        }.onCompletion {
            //关闭Loading弹窗
            println("end...")
        }.catch {t: Throwable ->
            // 捕获发送方的异常
            error.invoke(t)
        }
}

上面这个方法是针对请求的简单封装, 请求req 是一个 suspend修饰的挂起方法,方便我们使用,同时给 error 这个异常回调设置了一个默认值,这样如果我们不想特殊处理异常就比较方便,但是这里还有一个问题,下游的异常还是需要我们手动来写,这里我们写的是获取 req 的flow ,通过这个方法再写一个封装 resp 的flow 方法,直接try catch 收集方异常与中断处理

suspend fun <R> getFlowResp(
    req: suspend () -> R,
    error: (Throwable) -> Unit = {},
    result: (R) -> Unit
) = getFlowReq(req,error).collect{
    try {
        if(coroutineContext.isActive){
            result.invoke(it)
        }
    } catch (e: Exception) {
        println("下游遇到了异常")
        error.invoke(e)
    }
}

上面这个方法通过调用req 方法复用了他的逻辑,并且拦截了收集方的异常与异常的传递,还拦截了取消状态下已经进入管道的数据

先模拟一个接口方法,延时1s返回结果

suspend fun request(): TsmBean {
    delay(1000)
    return TsmBean("Tsm", true)
}

那么我们调用请求的方法如下

fun main() {
    runBlocking {
        launch(Dispatchers.IO) {
            getFlowResp(
                req = {
                    request()
                },
                error = {

                },
                result = {
                    println("result:${it.toString()}")
                })
        }
        delay(4000)
    }
}

如果我们不需要特殊处理异常,代码就非常简单了 如下

runBlocking {
    launch(Dispatchers.IO) {
        getFlowResp({
            request()
        }) {
            println("result:${it.toString()}")
        }
    }
    delay(4000)
}

基本上所有可能发生异常的地方都已经被捕获了,而且他的书写方式也是非常的简单易懂,flow 还是非常强大的

其实在 将MvvM 与 ViewBinding 协程等结合起来使用中还遇到了一些问题,那就是 ViewBinding 与 ViewModel 封装起来很不方便,后来使用了反射的方式解决了这个问题,让代码量大大减少

这里放一下我的封装简化版

/**
 * 使用类委托,简化使用逻辑
 */
open class BaseCoroutineActivity : AppCompatActivity() ,CoroutineScope by MainScope() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        委托后,直接持有一个 MainScope ,可以直接使用 launch 等方法
//        launch {
//
//        }
    }
    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}

基类是一个 使用委托实例化了一个 MainScope,方便后续使用

/**
 *  使用泛型初始化 ViewModel
 */
open class BaseViewModelActivity<E :ViewModel> : BaseCoroutineActivity() {

    lateinit var viewModel:E
    @CallSuper
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val types=(javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
        val type1 :Class<E> = types[0] as Class<E>
        viewModel= ViewModelProvider(this)[type1]
    }
}

基于 Base 又封装了一层泛型初始化 ViewModel ,

/**
 * 在ViewModel 的基础上增加使用泛型初始化 ViewBinding
 */
open class BaseMvvmActivity<E :ViewModel,T : ViewBinding> : BaseViewModelActivity<E>() {

    lateinit var  mBinding: T

    @CallSuper
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val types=(javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
        val type :Class<T> =types[1] as Class<T>
        var method = type.getDeclaredMethod("inflate", LayoutInflater::class.java)
        mBinding= method.invoke(this,layoutInflater) as T
        setContentView(mBinding!!.root)
    }
}

基于 ViewModel 又封装了一下 ViewBinding ,还是使用泛型初始化,简化后续使用逻辑