EventBus implementation using Coroutines and LiveData

Viewed 847

I need to broadcast events from different places within my app, and I need these events to be listened by different ViewModels. What I did is that I created a "custom" implementation of EventBus using Kotlin Coroutines, Channel more specifically. The implementation looks like this:

interface FlowEventBus {

    sealed class MessageEvent {
        data class MessageA(
            val someData: Int
        ) : MessageEvent()
        data class MessageB(
            val someOtherData: String
        ) : MessageEvent()
        object MessageC : MessageEvent()
    }

    suspend fun postMessage(messageEvent: MessageEvent)
    fun flowMessage(): Flow<MessageEvent>
}

class FlowEventBusImpl() : FlowEventBus {
    private val channel = Channel<FlowEventBus.MessageEvent>()

    override suspend fun postMessage(messageEvent: FlowEventBus.MessageEvent) {
        channel.send(messageEvent)
    }

    override fun flowMessage(): Flow<FlowEventBus.MessageEvent> {
        return channel.receiveAsFlow()
    }
}

Then in my ViewModels I use it like this

@HiltViewModel
class SomeViewModel @Inject constructor(
    private val flowEventBus: FlowEventBus
) : ViewModel() {
    
    val message = flowEventBus.flowMessage().asLiveData()
    // ...

The idea is that whatever Activity or Fragment is using this ViewModel, they can observe the message property and react to the "EventBus" events.

What's the problem?

Some of these events are unreliable. For example, let's say ActivityA is observing messages and it prompts a Snackbar every time we get a MessageC event. If the FlowEventBus broadcasts a MessageC event twice we'd only see the Snackbar pop once. I'm not very savvy about Kotlin Coroutines yet, what I think might be happening is the classing SingleLiveEvent scenario. My guess is that the asLiveData() extension turns the Flow into a MutableLiveData and if we set the same value twice, it will just ignore it. But I'm not sure how to introduce SingleLiveEvent here.

Any feedback is welcome, Thanks!

1 Answers

Despite it is not a direct answer to your question... But

Why do you need the conversion from flow to live data? Just subscribe to flow on ui layer and populate the flow with necessary livedata features. Believe me, this will make your life much more easier.

I offer to do it by using implementation androidx.lifecycle:lifecycle-runtime-ktx:x.y.z and SharedFlow

In fragment:

with(viewModel) {
    eventsFlow.collectWhenStarted(viewLifecycleOwner, ::processEvents)
}

collectWhenStarted is a utility method which i offer to simplify subscribing a bit. It utilises using of launchWhenStarted method from the library:

inline fun <T> Flow<T>.collectWhenStarted(
    lifecycleOwner: LifecycleOwner,
    crossinline action: suspend (T) -> Unit
): Job = lifecycleOwner.lifecycleScope.launchWhenStarted {
    collectLatest {
        action.invoke(it)
    }
}

You can create similar utilities for other lifecycle events

To substitute a LiveData feature of persisting the data between configuration changes, background/foreground changes just convert a regular flow from domain layer to SharedFlow with replay. In a view model:

val eventsFlow: Flow<DataClass> = flowEventBus
    .flowMessage()
    .shareIn(viewModelScope, SharingStarted.Lazily, 1)

In case you are not distinct the emitted elements your EventBus shouldn't exclude any item you passed there

Check this article, the approach described there is close to what i suggest

Related