Пост 10. О полезных иногда EventBus-ах
Думаю многие уже знают или со временем столкнуться с шинами или EventBus-ами. Сейчас, по большому счету - это антипаттерн.
Если совсем упрощать, то суть вот в чем: кто-то кидает события, а кто-то на них подписывается.
Те кто давно в Android-разработке еще помнят времена библиотеки от GreenRobot: https://github.com/greenrobot/EventBus и то как все начали обмазываться шинами и использовать их на все случаи жизни.
Солидное количество звезд в репе, казалось бы. Но проблем с ней больше чем плюсов. Конечно, если вы один пишите код, то даже такая версия может пригодится. Но стоит подумать и о будущем кода. Кто-то до вас писал код или кто-то пишет с вами параллельно. Вам нужно его поддерживать. Основная проблема EventBus - это отладка ошибок и превращение кода в макароны. Любой кусок кода может подписаться на события и как-то их обработать и выглядит это довольно не явно. Поэтому стоит подумать дважды прежде чем использовать этот антипаттерн.
Но иногда эти шины могут быть полезны, при условии, что будут более явными.
На корутинах можно сделать похожий EventBus без каких-либо отдельных библиотек (кроме корутиновских естественно) и всего в несколько строчек:
@OptIn(ExperimentalCoroutinesApi::class)
abstract class EventBus<T>(type: Type = Type.PUBLISH) : BroadcastChannel<T> by BroadcastChannel(
when (type) {
Type.PUBLISH -> 1
Type.BEHAVIOR -> CONFLATED
}
) {
val scope = CoroutineScope(IO)
fun sendMessage(message: T) {
scope.launch {
send(message)
}
}
open fun subscribe(): ReceiveChannel<T> {
return openSubscription()
}
enum class Type {
PUBLISH, BEHAVIOR
}
}
Подобный код мы использовали пару лет на проде и вцелом он вполне себе. На тот момент была классическая архитектура на базе MVP (Moxy + Cicherone)
Далее, прокидываем нашу шину в базовый Presenter, чтобы подписываться на новые события:
protected fun <T> consumeEach(
eventBus: EventBus<T>,
scope: CoroutineScope = uiScope,
block: suspend (T) -> Unit
) {
consumeEach(eventBus.subscribe(), scope, block)
}
protected fun <T> consumeEach(
channel: ReceiveChannel<T>,
scope: CoroutineScope = uiScope,
block: suspend (T) -> Unit
) {
subscriptions.add(channel.apply {
scope.launch {
consumeEach {
block(it)
}
}
})
}
Что делает нашу шину более явной?
Во-первых, в каждом презентере наглядно видно какой класс шины Inject-ится и используется.
У нас было несколько реализаций под разные типы событий. Например, одна из них просто открывает нужный нам экран в случае получения определенного события:
@Singleton
class RecipeNavigationBus @Inject constructor() : EventBus<RecipeNavigationBus.Page>() {
fun showRecipes() {
sendMessage(Page.RECIPES)
}
fun showCollections() {
sendMessage(Page.COLLECTIONS)
}
enum class Page {
RECIPES, COLLECTIONS
}
}
Как использовать.
Идея очень простая и по сути сводится к Dependency Injection. Инжектим в нужные места шину и один кусок кода подписывается на прослушку событий:
consumeEach(recipeBoxNavigationBus) {
when (it) {
Page.RECIPES ->
}
а другой кидает событие в шину (на самом деле в корутиновский канал):
io {
recipeBoxNavigationBus.showCollections()
}
Вообщем-то, на этом все.
Главное внимательно следить, чтобы корутины «подчищались» (cancel() +supervisorJob.cancelChildren()) и тогда все будет более-менее, но все же не стоит особо злоупотреблять подобными шинами. В очень редких кейсах стоит, но не более.