什么?你还在用EventBus吗?快来试试用Flow api搞一个!

4,601 阅读6分钟

1.前言

谷歌推出flow api已经很久了,俗称为数据流。

我们老规矩看下定义,数据流以协程为基础构建,可提供多个值。从概念上来讲,数据流是可通过异步方式进行计算处理的一组数据序列。例如,Flow<Int> 是发出整数值的数据流。

数据流与生成一组序列值的 Iterator 非常相似,但它使用挂起函数通过异步方式生成和使用值。这就是说,例如,数据流可安全地发出网络请求以生成下一个值,而不会阻塞主线程。这么一大堆,其实就是说,flow就像一个流一样,可以在其中生成数值,并且可在协程中使用,哎呦,官方是真的疼爱Coroutine。

基于flow api这种特性,我们可以在很多数据通讯场景中得到使用,同时由于flow 在协程场景下运用,可以用来封装很多有趣的东西,本文就带来了一个利用该api封装的总线型库。

2. 预演

前面讲了一大堆概念,那么我们该看一下具体代码表现了,首先是Flow

public interface Flow<out T> {
    @InternalCoroutinesApi
    public suspend fun collect(collector: FlowCollector<T>)
}

其实就是一个接口,里面提供了一个collect方法,作用很简单,就是收集数据,还有就是注意它是一个suspend方法,目的就是想要规范在协程内使用,本篇不去深究协程和suspend的种种关系,因为不是这次的重点。也就是说collect运行在某个协程内部,ok搞定。注意看参数FlowCollector,那么这是个什么东西呢?

别急,在下面

public interface FlowCollector<in T> {
    public suspend fun emit(value: T)
}

FlowCollector接口里面定义了flow api的发送规范,注意其也是一个suspend方法,它就属于消息的发送方。 现在我们理清楚了,原来就是定义了一个接收方跟发送方的规范,这里也能看出来为什么普通flow是个冷流(注意是普通,flow可以实现热流,比如StateFlow),你看,flow接口中的collect,其实就是接受一个FlowCollector,调用其里面的emit才会发送数据。还有一个小细节,就是flow里面的泛型是out修饰,然而FlowCollector是in修饰,这里可以留给读者们思考为什么这么设计。

3.跟现有的数据类型区别

StateFlow、Flow 和 LiveData

因为我们接下来会运用到SharedFlow,所以先过一下区别

StateFlow 和 LiveData 具有相似之处。两者都是可观察的数据容器类,并且在应用架构中使用时,两者都遵循相似模式。

但请注意,StateFlow 和 LiveData 的行为确实有所不同:

  • StateFlow 需要将初始状态传递给构造函数,而 LiveData 不需要。
  • 当 View 进入 STOPPED 状态时,LiveData.observe() 会自动取消注册使用方,而从 StateFlow 或任何其他数据流收集数据的操作并不会自动停止。 【来自Android开发者官网】 那么StateFlow和SharedFlow的区别呢
public interface StateFlow<out T> : SharedFlow<T> {
   
    public val value: T
}

看到上面代码就水落石出了,SharedFlow是StateFlow更高一级的抽象,而SharedFlow是

public interface SharedFlow<out T> : Flow<T> {
    /**
     * A snapshot of the replay cache.
     */
    public val replayCache: List<T>
}

可以看到,里面存在着一个list对象,这就说明了数据流的来源了,其实最本质的数据结构就是一个list。看到这里,是不是很想我们总线型的框架内部核心了呢,那么我们开始封装我们的总线数据流吧

4.开始搞事情

我们需要实现的东西是啥: 1.总线型数据流,内部可以用flow api实现,这里可以采用SharedFlow。 2.相较于EventBus需要注册解除注册,相信各位大佬们肯定很烦了,那么我们就需要自动注册解除注册这种功能啦。 3.可以发送粘性事件与非粘性事件 4.切换线程订阅.. 5.足够精简 6.实现发送和接收核心功能

我们基于上面的问题,开始一步步实现:

Q1:根据上面源码分析,SharedFlow里面就维护着一个list,满足了我们消息存放的数据结构,所以没问题

Q2:自动注册解除注册这个,嘿嘿,大家看都不用看,就会想到LifecycleEventObserver这个接口,毕竟监听生命周期嘛,老套路了,那么我们哪里实现这个接口呢?flow?事件里面?nonono,我们一直忽略了一个重要的点,就是协程,上面说了一堆东西,其实flow运行在协程里面对不对,我们可以在协程里面做文章呀!也就是说,我们控制了协程生命周期,不就是控制了flow的生命周期了嘛!


class LifeCycleJob(private val job: Job) : Job by job, LifecycleEventObserver {
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            this.cancel()
        }
    }

    override fun cancel(cause: CancellationException?) {
        if (!job.isCancelled) {
            job.cancel()
        }
    }

}

Q3:如何区分粘性事件和非粘性事件,这个也很好处理,粘性数据的话,就让接收方接受数据的时候,把list里面的数据再发送一遍不就可以了,非粘性订阅就不发送由于我们现在想要单个事件的订阅所以可以利用 MutableSharedFlow实现

@Suppress("FunctionName", "UNCHECKED_CAST")
public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {
    require(replay >= 0) { "replay cannot be negative, but was $replay" }
    require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative, but was $extraBufferCapacity" }
    require(replay > 0 || extraBufferCapacity > 0 || onBufferOverflow == BufferOverflow.SUSPEND) {
        "replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy $onBufferOverflow"
    }
    val bufferCapacity0 = replay + extraBufferCapacity
    val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow
    return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow)
}

看到了MutableSharedFlow实现实现了MutableSharedFlow接口,而MutableSharedFlow接口又实现了SharedFlow,FlowCollector,所以可以充当发送者与收集者啦!

interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T>

看到MutableSharedFlow参数,所以我们可以直接利用其特性: 对于事件集合,我们可以这样定义消息数据集合:

非粘性
var events = ConcurrentHashMap<Any, MutableSharedFlow<Any>>()
    private set

粘性
var stickyEvents = ConcurrentHashMap<Any, MutableSharedFlow<Any>>()
    private set

Q4:切换线程,这个不就是协程最擅长的嘛,利用Dispatch切换指定响应线程即可

Q5:足够精简的话,这里采用扩展函数,就可以非常方便原有功能的拓展。

Q6:前面说了MutableSharedFlow实现了flow发送和接收的接口,所以我们可以利用这个特性


inline fun <reified T> post(event: T, isStick: Boolean) {
    val cls = T::class.java
    if (!isStick) {
        stickyEvents.getOrElse(cls) {
            MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)
        }.tryEmit(event as Any)
    } else {
        stickyEvents.getOrElse(cls) {
            MutableSharedFlow(1, 1, BufferOverflow.DROP_OLDEST)
        }.tryEmit(event as Any)

    }

}

那么接收呢,接收就有点麻烦了,因为我们发送都是用的post方法,于是如何区分粘性消息和非粘性消息呢?这里我们采用都监听的方式,那么是在同一个协程域里面监听还是不同的监听呢?这里又涉及到了一个小问题,collect函数会挂起当前协程,所以如果采用同一个协程域内监听的话,显然是不行的,因为同一个协程域内(不考虑存在子协程域)的情况下,其实运行是串行的,所以我们需要开两个协程域,分别在里面调用collect函数,监听粘性事件与非粘性事件

graph TD
协程域1 -->监听非粘性事件 --> Stop

graph TD
协程域2 -->监听粘性事件 --> Stop
inline fun <reified T> onEvent(
    event: Class<T>,
    crossinline dos: (T) -> Unit,
    owner: LifecycleOwner,
    env: SubscribeEnv
) {
    if (!events.containsKey(event)) {
        events[event] = MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)
    }
    if (!stickyEvents.containsKey(event)) {
        stickyEvents[event] = MutableSharedFlow(1, 1, BufferOverflow.DROP_OLDEST)
    }
    val coroutineScope: CoroutineScope = when (env) {
        SubscribeEnv.IO -> CoroutineScope(Dispatchers.IO)
        SubscribeEnv.DEFAULT -> CoroutineScope(Dispatchers.Default)
        else -> CoroutineScope(Dispatchers.Main)
    }

    coroutineScope.launch {
        events[event]?.collect {
            if (it is T) {
                dos.invoke(it)
            }
        }

    }.setLifeCycle(owner.lifecycle)

    coroutineScope.launch {
        stickyEvents[event]?.collect {
           
            if (it is T) {
                dos.invoke(it)
            }
        }
    }.setLifeCycle(owner.lifecycle)


}

最后总结

到这个我们已经可以基本实现一个总线型数据流库啦,快用起来,其实这个也是pigeon的核心代码,最后附上github地址,希望大家点个赞!又是卑微求star的一天 github.com/TestPlanB/p…