EventBus е библиотека за Android, която имплементира шаблона Publisher-Subscriber чрез шина за събития, позволявайки обмен на данни между компоненти без пряка зависимост. Разработена от GreenRobot, библиотеката опростява комуникацията между Activity, Fragment, Service и Background Thread. Според данни от GitHub (2025), EventBus има над 25 хиляди звезди и се използва в хиляди Android приложения. Основните операции са subscribe (абонамент за събитие), post (изпращане на събитие) и sticky event (отложено събитие за нови абонати).
Основни точки
EventBus е библиотека за шина за събития за Android, която имплементира шаблона Publisher-Subscriber (издател-абонат). Тя позволява предаване на събития между компонентите на приложението (Activity, Fragment, Service, ViewModel) без създаване на изрични зависимости между тях. За разлика от стандартните Android механизми (Intent, BroadcastReceiver), EventBus работи вътре в процеса и не използва IPC. Библиотеката е оптимизирана за производителност и не използва рефлексия, когато Subscriber Index е правилно конфигуриран.
Архитектурата на EventBus се състои от три ключови елемента: Event (POJO клас с данни), Subscriber (обект с методи, маркирани с @Subscribe) и EventBus (централен диспечер). Абонатът се регистрира чрез EventBus.getDefault().register(this), отписване — чрез unregister(this). Събитията са типизирани: обработчиците се абонират за конкретен клас събитие и се извикват само при post на събитие от този клас или неговите подкласове.
// POJO събитие
data class MessageEvent(
val message: String,
val timestamp: Long = System.currentTimeMillis()
)
// Абонат в Activity
class MainActivity : AppCompatActivity() {
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(event: MessageEvent) {
textView.text = event.message
}
}
// Изпращане на събитие от друг компонент
EventBus.getDefault().post(MessageEvent("Hello from Service"))
По подразбиране EventBus използва рефлексия за намиране на @Subscribe методи при register(). Subscriber Index генерира индекс на обработчиците във фазата на компилация чрез процесор на анотации. Това елиминира overhead-а на рефлексията и ускорява регистрацията. За активиране добавете eventbus-annotation-processor в build.gradle. EventBus автоматично използва индекса, ако е наличен в classpath. Без индекс библиотеката все още работи, но с леко намалена производителност.
При извикване на EventBus.getDefault().post(event), библиотеката определя типа на събитието, намира всички регистрирани абонати с @Subscribe методи, приемащи този тип, и ги извиква според указания ThreadMode. Търсенето на абонати се извършва по картата Class → CopyOnWriteArrayList
Абонатът трябва да се регистрира в onStart() и да се отпише в onStop(). Ако се регистрира в onCreate() и се отпише в onDestroy(), Activity, унищожена без извикване на onDestroy (поради finish()), може да остане в списъка с абонати. Изтичане на абонат — един от основните проблеми на EventBus: Activity, останала в списъка с абонати, няма да бъде събрана от GC, докато не се отпише. Винаги сдвоявайте register/unregister в правилните lifecycle методи.
Анотацията @Subscribe поддържа параметър priority (цяло число, по подразбиране 0). Обработчиците с по-висок приоритет се извикват по-рано. cancelEventDelivery() позволява прекъсване на доставката на събитието до останалите абонати. Това е полезно за приоритетни обработчици (логване, удостоверяване), които могат да отменят обработката на събитието от по-нисши абонати. Функцията е достъпна само в нишката на изпращане на събитието.
// Сложен пример с приоритет
data class NavigationEvent(val screen: String, val data: Bundle)
class NavigationInterceptor {
@Subscribe(priority = 10, threadMode = ThreadMode.POSTING)
fun onNavigationEvent(event: NavigationEvent) {
if (event.screen == "restricted" && !isAuthorized) {
EventBus.getDefault().cancelEventDelivery(event)
}
}
}
class AnalyticsLogger {
@Subscribe(priority = 5)
fun logNavigation(event: NavigationEvent) {
analytics.logScreen(event.screen)
}
}
// Изпращане на събитие
EventBus.getDefault().post(NavigationEvent("profile", bundle))
Android предлага няколко механизма за вътрешнопроцесна комуникация: EventBus, LocalBroadcastManager (остарял) и LiveData/Flow. Всеки има своите предимства и недостатъци. Изборът зависи от архитектурния подход и изискванията за производителност. Съвременните препоръки на Google клонят към LiveData и Flow поради интеграция с Lifecycle и липса на изтичания.
| Характеристика | EventBus | LocalBroadcastManager | LiveData / Flow |
|---|---|---|---|
| Типизация | Чрез клас на събитие | Чрез Intent filter (String) | Чрез generic тип |
| Lifecycle-aware | Не (ръчно отписване) | Не (ръчно отписване) | Да (автоматично) |
| Sticky | Да (postSticky) | Не | Да (LiveData — винаги sticky) |
| ThreadMode | MAIN, POSTING, BACKGROUND, ASYNC | Само main | Чрез observe/observeOn |
| Производителност | Висока (Subscriber Index) | Средна (IPC обвивка) | Висока (наблюдение) |
EventBus е полезен в проекти с наследен код (legacy code) и там, където LiveData/Flow не са достъпни (Java-only проекти). Sticky events на EventBus дават гъвкавост, която липсва в LocalBroadcastManager. EventBus също е по-прост за изпращане на събития от Service към Activity без ViewModel — особено когато трябва да уведомите за напредъка на фонова задача. Библиотеката има минимален размер (около 50 KB) и не добавя зависимости.
LiveData и Flow са част от Android Jetpack и са интегрирани с Lifecycle. Те автоматично се отписват при унищожаване на компонента, елиминирайки изтичания на памет. Flow поддържа корутини и сложни оператори за трансформация. Google препоръчва LiveData за UI слоя и Flow за хранилища. EventBus остава за междумодулни събития, където навигацията и бизнес логиката не се вписват в MVVM.
Subscribe — регистрация на обработчик на събитие чрез анотация @Subscribe. Методът трябва да бъде public, void и да приема точно един параметър — типа на събитието. Post — изпращане на събитието до всички абонирани обработчици чрез EventBus.getDefault().post(event). Методът post не връща резултат и не информира колко обработчика са били извикани. За събития с отговор използвайте отделен Event клас с поле за резултата.
Събитие — всеки Java/Kotlin клас. Препоръчително е използването на data class за неизменяеми събития и обикновен клас за събития с променливи полета. Именуването на събития трябва да отразява действието: UserLoggedInEvent, DataLoadedEvent, NetworkErrorEvent. Избягвайте един общ Event клас с поле String type — това лишава от предимствата на типизацията. Йерархия на събития (родителски Event) позволява абониране за група сродни събития.
// Йерархия на събития
open class UserEvent
data class UserLoggedIn(val userId: String) : UserEvent()
data class UserLoggedOut(val reason: String) : UserEvent()
// Абониране за базов клас
class SessionManager {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUserEvent(event: UserEvent) {
when (event) {
is UserLoggedIn -> startSession(event.userId)
is UserLoggedOut -> endSession(event.reason)
}
}
}
// Изпращане
EventBus.getDefault().post(UserLoggedIn("user_123"))
Извикването на EventBus.getDefault().register(this) сканира класа на абоната чрез рефлексия или Subscriber Index и запазва намерените @Subscribe методи в картата на събитията. Unregister премахва абоната от картата. Повторна регистрация без отписване — грешка (ще се появи MultipleSubscriberException). За Fragment се регистрирайте в onStart() и се отпишете в onStop(). За Service — в onCreate() и onDestroy(). За ViewModel не се препоръчва — използвайте LiveData.
Sticky event — събитие, което се запазва в EventBus след изпращане. Нови абонати, регистрирани след postSticky(), незабавно получават последното sticky-събитие от съответния тип. Това е удобно за предаване на начално състояние: при отваряне на екран, той получава последните данни, изпратени преди неговата регистрация. Премахване на sticky-събитие може да се направи чрез EventBus.getDefault().removeStickyEvent(Class).
ThreadMode определя в коя нишка се извиква обработчикът. POSTING (по подразбиране) — обработчикът се изпълнява в същата нишка, в която е извикан post. MAIN — обработчикът се изпълнява в main нишка чрез Handler. BACKGROUND — обработчикът се изпълнява във фонова нишка; ако post е извикан в main нишка, EventBus поставя обработчика в опашката на фоновата нишка. ASYNC — всеки обработчик се изпълнява в отделна фонова нишка от пула от нишки. За актуализации на UI използвайте MAIN.
// Sticky Event
data class LocationEvent(val lat: Double, val lng: Double)
// Изпращане на sticky-събитие от LocationService
EventBus.getDefault().postSticky(LocationEvent(55.7558, 37.6173))
// Абонатът получава последното местоположение веднага след регистрация
class MapFragment : Fragment() {
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
// Веднага ще получи LocationEvent, ако е имало postSticky
}
override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onLocationEvent(event: LocationEvent) {
moveMapTo(event.lat, event.lng)
}
}
// Премахване на sticky-събитие
EventBus.getDefault().removeStickyEvent(LocationEvent::class.java)
BACKGROUND използва една фонова нишка за всички обработчици — те се изпълняват последователно. ASYNC създава нова нишка от пула за всеки обработчик — те се изпълняват паралелно. BACKGROUND е подходящ за входно-изходни операции със споделена база данни. ASYNC — за независими дълги операции (мрежови заявки). И двата режима изискват thread-safe достъп до споделени ресурси. Брой нишки: пулът ASYNC е неограничен.
При използване на EventBus разработчиците често допускат грешки, водещи до изтичане на памет, неочаквани извиквания и спад на производителността. Най-критичните: забравено отписване в Activity, регистрация в onCreate (а не onStart/onStop), абониране за Object (всички събития), изпращане на събития в безкраен цикъл. Профилирането чрез Android Profiler помага за идентифициране на проблеми.
Най-честата грешка — регистрация на Activity в onCreate() без отписване в onDestroy(). Резултат: EventBus съхранява референция към Activity, GC не може да я освободи. При завъртане на екрана се създава ново Activity, предишното остава в паметта. Решение: винаги сдвоявайте register/unregister в onStart/onStop. За Fragment използвайте същата схема. Ако Activity се задържа от EventBus след finish, проверете чрез Memory Profiler.
Без Subscriber Index EventBus използва рефлексия за намиране на @Subscribe методи при всяко register(). На устройства с Android 6-7 рефлексията работи бавно, причинявайки закъснения до 50 ms. Subscriber Index напълно елиминира рефлексията: методите се индексират във фазата на компилация чрез процесор на анотации. За проекти с 20+ абонати индексът е задължителен. Проверете дали kapt или annotationProcessor е свързан в build.gradle.
// build.gradle (app) — свързване на Subscriber Index
dependencies {
implementation 'org.greenrobot:eventbus:3.3.1'
annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.3.1'
}
// За Kotlin използвайте kapt
plugins {
id 'kotlin-kapt'
}
dependencies {
implementation 'org.greenrobot:eventbus:3.3.1'
kapt 'org.greenrobot:eventbus-annotation-processor:3.3.1'
}
// Конфигурация на индекса (в defaultConfig)
kapt {
arguments {
arg('eventBusIndex', 'com.app.EventBusIndex')
}
}
Съвременните проекти на Kotlin и Jetpack Compose предпочитат SharedFlow и Channel от библиотеката kotlinx.coroutines. SharedFlow поддържа replay (sticky), buffering и backpressure. Channel — еднократни събития (toast, навигация). И двете решения са интегрирани с Lifecycle чрез repeatOnLifecycle и не изискват ръчно отписване. За нови проекти се препоръчва SharedFlow вместо EventBus. За съществуващи проекти миграцията е оправдана при рефакториране.
Често задавани въпроси
EventBus е шина за събития за обмен на данни между всякакви компоненти (Activity, Fragment, Service). LiveData е lifecycle-aware обвивка за данни, наблюдавани от UI компонент. LiveData автоматично управлява абонамента чрез Lifecycle. EventBus изисква ръчно register/unregister. LiveData се препоръчва за UI слоя, EventBus — за междумодулна комуникация, където LiveData е неудобен.
Sticky event — събитие, което се запазва в EventBus след изпращане. Нови абонати, регистрирани след postSticky(), незабавно получават последното sticky-събитие. Използва се за начално състояние: при отваряне на екран, той получава последните данни без повторна заявка. Премахва се чрез removeStickyEvent() или при изпращане на ново sticky-събитие от същия тип.
Да, EventBus е thread-safe. Извикването на post() е възможно от всяка нишка. Доставката на събитие до абонати се синхронизира вътре в библиотеката. ThreadMode определя нишката на изпълнение на обработчика: MAIN (main нишка чрез Handler), POSTING (нишка на изпращача), BACKGROUND (опашка от фонови задачи), ASYNC (отделна нишка). За актуализации на UI използвайте MAIN, за тежки операции — ASYNC.
Активирайте логване чрез EventBus.builder().logNoSubscriberMessages(true).sendNoSubscriberEvent(true).install(). Абонирайте се за NoSubscriberEvent за проследяване на събития без обработчици. Използвайте SubscriberExceptionEvent за глобално обработване на изключения. Android Profiler помага за намиране на изтичания. За сложни сценарии напишете тест: EventBus.getDefault().register(mock) + post(event) + verify(mock).
Не, EventBus (GreenRobot) е обвързан с Android SDK и JVM. За Kotlin Multiplatform използвайте Kotlin Multiplatform SharedFlow или KMMBus — библиотеки, поддържащи споделен код. EventBus от страната на Android на KMM проект работи, но не е достъпен в commonMain. За междуплатформени събития се предпочитат родните механизми на платформата или абстракция чрез expect/actual.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.