戰(zhàn):EventBus發(fā)布訂閱模式從原理到避坑指南)
1. 跨組件通信的痛點(diǎn)與EventBus的定位Flutter 開發(fā)到一定階段必然會(huì)遇到一個(gè)繞不開的問題兩個(gè)毫不相干的 Widget 之間怎么傳數(shù)據(jù)。比如用戶在“個(gè)人中心”頁面點(diǎn)了退出登錄結(jié)果“首頁”“消息頁”“訂單頁”都得同步刷新狀態(tài)又比如一個(gè)全局的購物車角標(biāo)散落在四五個(gè) Tab 里任何一個(gè)地方加減商品其他位置都得跟著變。這種場景用setState顯然不現(xiàn)實(shí)用InheritedWidget或各種狀態(tài)管理庫又顯得殺雞用牛刀尤其是當(dāng)通信雙方在組件樹上隔了十萬八千里、根本沒有父子關(guān)系的時(shí)候。EventBus 就是在這個(gè)縫隙里生長出來的方案。它的本質(zhì)是一個(gè)全局的發(fā)布訂閱中心一個(gè)地方發(fā)事件任意多個(gè)地方監(jiān)聽事件發(fā)布者和訂閱者互相不認(rèn)識(shí)、不依賴、不耦合。你可以把它想象成小區(qū)樓下的公告欄——物業(yè)貼一張停水通知誰路過誰看不需要物業(yè)挨家挨戶敲門也不需要住戶之間互相轉(zhuǎn)告。Flutter 生態(tài)里最常用的實(shí)現(xiàn)是event_bus這個(gè)包代碼量極小核心就一個(gè)EventBus類加一個(gè)EventBusSubscriber注解但用好了能省下大量樣板代碼。這篇文章適合三類人看一是剛接觸 Flutter、被跨組件通信折磨過的新手二是用setState和回調(diào)傳參傳到崩潰、想找個(gè)輕量方案的中級(jí)開發(fā)者三是已經(jīng)在用狀態(tài)管理庫、但想搞清楚 EventBus 到底該不該引入、什么時(shí)候引入的老手。我會(huì)從設(shè)計(jì)思路講到源碼級(jí)細(xì)節(jié)再給出一套可以直接抄的實(shí)操方案最后把踩過的坑和排查技巧全盤托出。全程不堆砌術(shù)語盡量用生活化的類比把原理講透。2. 為什么是EventBus方案選型背后的取舍邏輯2.1 跨組件通信的幾種主流方案對(duì)比在決定用 EventBus 之前得先搞清楚它在整個(gè)方案譜系里的位置。Flutter 里跨組件通信大致有這么幾條路InheritedWidget及其衍生、Provider/Riverpod這類狀態(tài)管理、GlobalKey、回調(diào)函數(shù)層層傳遞、以及 EventBus。它們不是互相替代的關(guān)系而是各有各的適用半徑。方案耦合度適用距離學(xué)習(xí)成本典型場景回調(diào)傳參高父子/近親低簡單父子通信InheritedWidget中祖先-后代中主題、配置下發(fā)Provider/Riverpod中全局中高復(fù)雜狀態(tài)共享GlobalKey高任意低調(diào)用子組件方法EventBus極低任意極低事件通知、狀態(tài)廣播從表里能看出來EventBus 最大的特點(diǎn)是耦合度極低和學(xué)習(xí)成本極低。它不關(guān)心組件樹結(jié)構(gòu)不要求發(fā)布者和訂閱者有血緣關(guān)系甚至不要求它們?cè)谕粋€(gè)頁面。代價(jià)是什么呢代價(jià)是它不管理狀態(tài)只傳遞事件。這句話很關(guān)鍵很多人用 EventBus 用出問題就是因?yàn)榘阉?dāng)狀態(tài)管理用了。2.2 EventBus解決的核心問題解耦與廣播我舉個(gè)實(shí)際項(xiàng)目里的例子。某電商 App 有一個(gè)“加入購物車”的按鈕散落在商品詳情頁、搜索結(jié)果頁、推薦流里。每點(diǎn)一次底部 Tab 的購物車角標(biāo)要 1購物車頁面如果已經(jīng)打開要刷新列表個(gè)人中心里的“待結(jié)算”數(shù)量也要變。如果用回調(diào)你得從按鈕一路把回調(diào)傳到 Tab 欄中間隔了七八層 Widget代碼會(huì)臟得沒法看。如果用 Provider你得把購物車狀態(tài)提升到全局然后每個(gè)消費(fèi)點(diǎn)都去watch邏輯上沒問題但對(duì)于“點(diǎn)一下加一”這種簡單事件來說有點(diǎn)重。EventBus 的做法是按鈕點(diǎn)擊時(shí)EventBus.instance.fire(CartChangedEvent())然后 Tab 欄、購物車頁、個(gè)人中心各自onCartChangedEvent()監(jiān)聽。發(fā)布者不知道誰在聽訂閱者不知道誰發(fā)的雙方只認(rèn)事件類型。這種發(fā)布訂閱模式帶來的解耦是 EventBus 最核心的價(jià)值。它特別適合“一件事發(fā)生了多個(gè)地方需要知道”的廣播型場景。2.3 什么時(shí)候不該用EventBus這里必須潑一盆冷水。EventBus 不是銀彈濫用它會(huì)帶來兩個(gè)嚴(yán)重問題事件流難以追蹤和內(nèi)存泄漏。當(dāng)項(xiàng)目里到處是fire和on你很難搞清楚一個(gè)事件發(fā)出去之后到底觸發(fā)了哪些邏輯調(diào)試時(shí)像在黑暗中摸象。而且如果訂閱者忘記取消訂閱EventBus 會(huì)一直持有它的引用導(dǎo)致頁面銷毀后依然被回調(diào)輕則報(bào)錯(cuò)重則內(nèi)存泄漏。我的經(jīng)驗(yàn)判斷標(biāo)準(zhǔn)是這樣的如果兩個(gè)組件是明確的父子關(guān)系優(yōu)先用回調(diào)和setState如果是需要共享的狀態(tài)比如用戶信息、主題用 Provider 或 Riverpod只有當(dāng)通信雙方?jīng)]有穩(wěn)定關(guān)系、且傳遞的是一次性事件登錄成功、網(wǎng)絡(luò)斷開、刷新指令時(shí)才考慮 EventBus。把它當(dāng)成“廣播喇叭”而不是“數(shù)據(jù)庫”。3. EventBus核心機(jī)制與源碼級(jí)細(xì)節(jié)拆解3.1 發(fā)布訂閱模式的三個(gè)角色EventBus 的實(shí)現(xiàn)再簡單也逃不開發(fā)布訂閱模式的三個(gè)基本角色事件Event、發(fā)布者Publisher、訂閱者Subscriber。事件是一個(gè)普通的 Dart 對(duì)象通常是個(gè)空類或者帶數(shù)據(jù)的類發(fā)布者調(diào)用fire方法把事件丟進(jìn)總線訂閱者通過on方法注冊(cè)一個(gè)回調(diào)聲明自己關(guān)心哪種類型的事件??偩€內(nèi)部維護(hù)的是一個(gè)MapType, ListFunction結(jié)構(gòu)key 是事件類型value 是回調(diào)列表。當(dāng)fire被調(diào)用時(shí)總線根據(jù)事件的運(yùn)行時(shí)類型找到對(duì)應(yīng)的回調(diào)列表挨個(gè)執(zhí)行。這個(gè)設(shè)計(jì)非常輕量沒有中間件、沒有隊(duì)列、沒有異步調(diào)度就是同步遍歷調(diào)用。理解這一點(diǎn)很重要因?yàn)樗鼪Q定了 EventBus 的行為特征同步執(zhí)行、順序調(diào)用、無緩沖。3.2 event_bus包的源碼結(jié)構(gòu)event_bus這個(gè)包的源碼短到可以一口氣讀完。核心就兩個(gè)文件event_bus.dart和event_bus_subscriber.dart。EventBus類里有一個(gè)_streamController類型是StreamControllerdynamic.broadcast()。對(duì)它底層用的是 Dart 的Stream 廣播流。onT()方法本質(zhì)上是在這個(gè)廣播流上做where過濾只保留類型為 T 的事件然后監(jiān)聽。fire()方法就是往流里add一個(gè)事件。這個(gè)實(shí)現(xiàn)方式帶來幾個(gè)關(guān)鍵特性。第一因?yàn)槭菑V播流所以支持多個(gè)訂閱者同時(shí)監(jiān)聽同一事件。第二因?yàn)槭?Stream所以它天然支持StreamSubscription的取消機(jī)制這就是為什么訂閱后必須cancel。第三fire是同步的事件會(huì)立即分發(fā)給所有訂閱者不會(huì)等到下一個(gè)事件循環(huán)。如果你需要異步得自己在回調(diào)里做處理。3.3 泛型與事件類型的設(shè)計(jì)要點(diǎn)事件類型的設(shè)計(jì)是 EventBus 使用中最容易被忽視、也最容易出問題的地方。因?yàn)榭偩€是按運(yùn)行時(shí)類型匹配的所以事件類的繼承關(guān)系會(huì)直接影響匹配結(jié)果。舉個(gè)例子如果你定義了一個(gè)基類BaseEvent然后LoginEvent extends BaseEvent那么onBaseEvent()能不能收到LoginEvent呢答案是不能。因?yàn)閣here過濾用的是精確類型匹配LoginEvent的運(yùn)行時(shí)類型是LoginEvent不等于BaseEvent。這個(gè)坑我踩過。當(dāng)時(shí)想做一個(gè)統(tǒng)一的事件基類結(jié)果發(fā)現(xiàn)子類事件根本收不到。解決辦法有兩個(gè)要么每個(gè)事件類型單獨(dú)監(jiān)聽要么在fire的時(shí)候顯式指定類型。我個(gè)人的建議是事件類盡量扁平不要搞繼承體系一個(gè)事件一個(gè)類名字起清楚比如UserLoginEvent、UserLogoutEvent、CartUpdatedEvent。這樣雖然類多了點(diǎn)但匹配邏輯清晰不會(huì)出意外。4. 從零搭建一套可復(fù)用的EventBus方案4.1 依賴引入與基礎(chǔ)封裝第一步當(dāng)然是引入依賴。在pubspec.yaml里加上event_bus版本用當(dāng)前穩(wěn)定版即可。然后我建議不要直接在業(yè)務(wù)代碼里到處EventBus()而是做一個(gè)全局單例封裝。原因很簡單如果每個(gè)地方都new EventBus()那就是多個(gè)獨(dú)立的總線A 發(fā)的消息 B 根本收不到。全局單例能保證整個(gè) App 共用一條總線。// event_bus_manager.dart import package:event_bus/event_bus.dart; class EventBusManager { EventBusManager._internal(); static final EventBusManager _instance EventBusManager._internal(); factory EventBusManager() _instance; final EventBus _bus EventBus(); void fireT(T event) _bus.fire(event); StreamT onT() _bus.onT(); }這個(gè)封裝很薄但好處是以后想換實(shí)現(xiàn)比如換成自定義的 StreamController只需要改這一個(gè)文件。另外我加了一個(gè)onT()返回 Stream 的方法方便配合StreamBuilder使用后面會(huì)講。4.2 事件類的定義規(guī)范事件類怎么定義直接決定了代碼的可維護(hù)性。我的規(guī)范是三條命名帶 Event 后綴、只放必要數(shù)據(jù)、盡量不可變。比如購物車變化事件只需要帶上變化后的數(shù)量不需要把整個(gè)購物車對(duì)象塞進(jìn)去因?yàn)橛嗛喺呖赡苤魂P(guān)心數(shù)量。class CartUpdatedEvent { final int totalCount; const CartUpdatedEvent(this.totalCount); } class UserLoginEvent { final String userId; const UserLoginEvent(this.userId); } class RefreshHomeEvent { const RefreshHomeEvent(); }注意const構(gòu)造函數(shù)能讓事件對(duì)象在編譯期確定減少運(yùn)行時(shí)開銷。對(duì)于不帶數(shù)據(jù)的事件直接const RefreshHomeEvent()就行。這里有個(gè)小技巧如果事件很多可以按業(yè)務(wù)模塊分文件存放比如cart_events.dart、user_events.dart避免一個(gè)文件幾百行。4.3 訂閱與取消訂閱的標(biāo)準(zhǔn)寫法訂閱的標(biāo)準(zhǔn)寫法是在initState里注冊(cè)在dispose里取消。這是鐵律沒有例外。我見過太多人只在initState里on忘了dispose里cancel結(jié)果頁面退出后事件還在觸發(fā)報(bào)setState called after dispose的錯(cuò)誤。class _HomePageState extends StateHomePage { late StreamSubscription _cartSub; late StreamSubscription _loginSub; override void initState() { super.initState(); _cartSub EventBusManager().onCartUpdatedEvent().listen((event) { if (!mounted) return; setState(() { _cartCount event.totalCount; }); }); _loginSub EventBusManager().onUserLoginEvent().listen((event) { if (!mounted) return; _loadUserData(event.userId); }); } override void dispose() { _cartSub.cancel(); _loginSub.cancel(); super.dispose(); } }這里有兩個(gè)細(xì)節(jié)值得說。第一回調(diào)里加了if (!mounted) return;這是雙保險(xiǎn)防止極端情況下 dispose 和事件觸發(fā)競爭。第二每個(gè)訂閱都用一個(gè)獨(dú)立的StreamSubscription變量持有方便統(tǒng)一取消。如果訂閱很多可以用一個(gè)ListStreamSubscription來管理dispose 時(shí)遍歷取消。5. 實(shí)操全流程一個(gè)購物車角標(biāo)同步的完整案例5.1 場景拆解與事件設(shè)計(jì)假設(shè)我們要做一個(gè)購物車角標(biāo)同步功能。需求是這樣的商品詳情頁有“加入購物車”按鈕點(diǎn)擊后底部 Tab 欄的購物車角標(biāo)數(shù)字要 1同時(shí)如果購物車頁面已經(jīng)打開列表要刷新。這個(gè)場景里發(fā)布者是商品詳情頁訂閱者是 Tab 欄和購物車頁三方互不相識(shí)。事件設(shè)計(jì)上我定義兩個(gè)事件CartCountChangedEvent負(fù)責(zé)角標(biāo)數(shù)字變化CartListRefreshEvent負(fù)責(zé)通知列表刷新。為什么不合并成一個(gè)事件因?yàn)榻菢?biāo)只關(guān)心數(shù)字列表關(guān)心的是數(shù)據(jù)職責(zé)分開更清晰。如果合并角標(biāo)訂閱者還得從事件里解析出數(shù)字多此一舉。class CartCountChangedEvent { final int count; const CartCountChangedEvent(this.count); } class CartListRefreshEvent { const CartListRefreshEvent(); }5.2 發(fā)布端商品詳情頁的觸發(fā)邏輯商品詳情頁的“加入購物車”按鈕點(diǎn)擊后先調(diào)接口接口成功返回最新的購物車數(shù)量然后 fire 兩個(gè)事件。注意這里有個(gè)順序問題一定要等接口成功后再 fire不能在點(diǎn)擊瞬間就 fire否則數(shù)量對(duì)不上。Futurevoid _addToCart() async { final result await cartApi.add(productId); if (result.success) { EventBusManager().fire(CartCountChangedEvent(result.totalCount)); EventBusManager().fire(const CartListRefreshEvent()); } }這里我用了result.totalCount而不是本地 1因?yàn)橘徫镘嚁?shù)量可能受其他端影響以服務(wù)端返回為準(zhǔn)最可靠。這是實(shí)戰(zhàn)經(jīng)驗(yàn)事件里帶的數(shù)據(jù)要盡量是權(quán)威數(shù)據(jù)不要帶本地推算值否則多端同步時(shí)會(huì)打架。5.3 訂閱端Tab欄角標(biāo)的監(jiān)聽實(shí)現(xiàn)Tab 欄通常在MainPage的initState里訂閱收到事件后setState更新角標(biāo)。角標(biāo)組件可以用Badge或者自己畫一個(gè)紅點(diǎn)。override void initState() { super.initState(); _cartSub EventBusManager().onCartCountChangedEvent().listen((event) { if (!mounted) return; setState(() { _cartCount event.count; }); }); }如果 Tab 欄是用BottomNavigationBar實(shí)現(xiàn)的角標(biāo)可以放在icon外面包一層Stack。這里有個(gè)性能小技巧如果角標(biāo)數(shù)字變化頻繁可以考慮用ValueNotifier配合ValueListenableBuilder只刷新角標(biāo)那一小塊而不是整個(gè) Tab 欄setState。不過對(duì)于購物車這種低頻變化直接setState完全夠用。5.4 訂閱端購物車頁面的刷新處理購物車頁面的刷新稍微復(fù)雜一點(diǎn)因?yàn)樗赡苌婕熬W(wǎng)絡(luò)請(qǐng)求。收到CartListRefreshEvent后不能直接setState而是要先請(qǐng)求最新列表數(shù)據(jù)再更新 UI。_cartListSub EventBusManager().onCartListRefreshEvent().listen((_) { if (!mounted) return; _loadCartList(); }); Futurevoid _loadCartList() async { final list await cartApi.getList(); if (!mounted) return; setState(() { _cartList list; }); }注意_loadCartList里也要判斷mounted因?yàn)榫W(wǎng)絡(luò)請(qǐng)求是異步的返回時(shí)頁面可能已經(jīng)銷毀。這個(gè)細(xì)節(jié)很多人會(huì)漏導(dǎo)致偶發(fā)的崩潰。5.5 參數(shù)計(jì)算與數(shù)量同步的邊界處理購物車數(shù)量同步有個(gè)邊界情況如果用戶在購物車頁刪除了商品角標(biāo)也要跟著變。這時(shí)候購物車頁從訂閱者變成了發(fā)布者刪除成功后 fire 一個(gè)CartCountChangedEvent。這就形成了一個(gè)雙向通信詳情頁發(fā)事件給購物車頁購物車頁也發(fā)事件給 Tab 欄。EventBus 天然支持這種多對(duì)多關(guān)系不需要額外處理。但要注意避免事件循環(huán)。如果購物車頁收到CartListRefreshEvent后刷新列表刷新完又 fire 一個(gè)CartCountChangedEvent而 Tab 欄收到后又觸發(fā)什么導(dǎo)致再 fire就可能死循環(huán)。我的做法是刷新類事件和變更類事件分開刷新事件只觸發(fā)數(shù)據(jù)拉取不反向 fire變更事件只在用戶主動(dòng)操作后 fire。這樣職責(zé)清晰不會(huì)繞圈。6. 常見問題與排查技巧實(shí)錄6.1 事件收不到的五種原因排查事件發(fā)出去了但訂閱者沒反應(yīng)這是最高頻的問題。我整理了一個(gè)排查表按可能性從高到低排列。排查項(xiàng)現(xiàn)象解決方法事件類型不匹配完全無反應(yīng)檢查 fire 和 on 的泛型是否一致訂閱未注冊(cè)完全無反應(yīng)確認(rèn) initState 里 on 已執(zhí)行總線實(shí)例不同完全無反應(yīng)確認(rèn)用的是同一個(gè)單例訂閱已取消之前能收現(xiàn)在不能檢查是否被提前 cancel事件被繼承子類事件收不到避免事件類繼承用扁平結(jié)構(gòu)其中“總線實(shí)例不同”是最隱蔽的。如果 A 頁面用EventBus()新建了一個(gè)實(shí)例B 頁面用單例那它們根本不在一條總線上。所以我在第 4 節(jié)強(qiáng)調(diào)一定要用全局單例封裝從源頭杜絕這個(gè)問題。6.2 內(nèi)存泄漏與重復(fù)訂閱的坑重復(fù)訂閱是指同一個(gè)頁面多次initState比如被 push 多次每次都注冊(cè)新的訂閱但舊的沒取消。結(jié)果一個(gè)事件觸發(fā)多次回調(diào)setState被調(diào)用多次輕則性能浪費(fèi)重則邏輯錯(cuò)亂。解決辦法是確保dispose里一定 cancel并且用late StreamSubscription變量持有引用。內(nèi)存泄漏則是訂閱者被總線持有頁面銷毀后無法回收。因?yàn)閺V播流的監(jiān)聽列表里存著回調(diào)回調(diào)又閉包持有了 State 對(duì)象形成引用鏈。只要 cancel 了這條鏈就斷了。所以cancel 不是可選項(xiàng)是必選項(xiàng)。我個(gè)人的習(xí)慣是寫完on之后立刻寫dispose里的cancel形成肌肉記憶。6.3 事件順序與異步陷阱EventBus 的fire是同步的意味著事件會(huì)立即按訂閱順序執(zhí)行回調(diào)。如果訂閱者 A 的回調(diào)里有耗時(shí)操作會(huì)阻塞訂閱者 B 的回調(diào)。這在大多數(shù)場景下沒問題但如果某個(gè)回調(diào)里做了重計(jì)算就會(huì)拖慢整個(gè)事件分發(fā)。解決辦法是把耗時(shí)邏輯放到Future.microtask或scheduleMicrotask里讓分發(fā)先走完。另一個(gè)陷阱是在 build 方法里 fire 事件。build 可能被調(diào)用多次每次 fire 都會(huì)觸發(fā)訂閱者導(dǎo)致無限循環(huán)或性能問題。記住fire 只應(yīng)該在用戶交互回調(diào)或異步任務(wù)完成后調(diào)用絕不能在 build、initState 的同步部分里調(diào)用。6.4 調(diào)試EventBus的實(shí)用技巧調(diào)試 EventBus 最頭疼的是不知道事件流經(jīng)了哪些地方。我的做法是在封裝層加日志開發(fā)模式下打印事件的類型和時(shí)間戳。void fireT(T event) { assert(() { debugPrint([EventBus] fire: ${T.toString()} at ${DateTime.now()}); return true; }()); _bus.fire(event); }用assert包裹release 模式下自動(dòng)移除不影響性能。另外可以做一個(gè)“事件監(jiān)視器”頁面在開發(fā)時(shí)把所有事件打印出來方便追蹤。如果項(xiàng)目里事件特別多建議給事件類加一個(gè)toString實(shí)現(xiàn)打印時(shí)能看到具體數(shù)據(jù)。7. 進(jìn)階玩法EventBus與狀態(tài)管理的邊界7.1 EventBus配合StreamBuilder的用法前面提到我在封裝里加了onT()返回 Stream這其實(shí)是為了配合StreamBuilder。有些場景下用StreamBuilder比手動(dòng)listen更簡潔尤其是當(dāng) UI 完全由事件驅(qū)動(dòng)時(shí)。StreamBuilderCartCountChangedEvent( stream: EventBusManager().onCartCountChangedEvent(), builder: (context, snapshot) { final count snapshot.data?.count ?? 0; return Badge(label: Text($count)); }, )這種寫法省去了initState和dispose里的訂閱管理StreamBuilder會(huì)自動(dòng)處理。但要注意StreamBuilder每次 rebuild 都會(huì)重新訂閱如果 stream 是每次新建的可能有問題。所以onT()返回的應(yīng)該是同一個(gè)廣播流而不是新建的。這也是為什么封裝層要持有_bus實(shí)例。7.2 事件命名空間與模塊化拆分項(xiàng)目大了之后事件類會(huì)越來越多容易命名沖突。我的做法是按模塊加前綴比如CartCountChangedEvent、UserLoginEvent、OrderPaidEvent。如果模塊特別多可以用命名空間類來組織。class CartEvents { static const countChanged cart.count.changed; }不過 EventBus 是按類型匹配的用字符串反而失去了類型安全。所以我更推薦直接用類名區(qū)分配合文件夾分模塊存放。比如events/cart/、events/user/每個(gè)文件夾里放對(duì)應(yīng)的事件類。這樣既類型安全又便于查找。7.3 從EventBus遷移到狀態(tài)管理的時(shí)機(jī)判斷什么時(shí)候該從 EventBus 遷移到 Provider 或 Riverpod我的判斷標(biāo)準(zhǔn)是當(dāng)事件開始攜帶狀態(tài)、且多個(gè)訂閱者需要基于同一份狀態(tài)做不同渲染時(shí)。比如購物車角標(biāo)如果只是顯示數(shù)字EventBus 夠了但如果購物車頁面、結(jié)算頁、個(gè)人中心都需要基于同一份購物車數(shù)據(jù)做復(fù)雜計(jì)算那就該用狀態(tài)管理了因?yàn)闋顟B(tài)管理能保證數(shù)據(jù)一致性而 EventBus 每次 fire 都是獨(dú)立事件訂閱者各自維護(hù)狀態(tài)容易不一致。遷移不是非此即彼。我現(xiàn)在的項(xiàng)目里EventBus 和 Riverpod 是共存的Riverpod 管狀態(tài)EventBus 管事件通知。比如用戶登錄后Riverpod 更新用戶狀態(tài)同時(shí) fire 一個(gè)UserLoginEvent通知那些不需要響應(yīng)式狀態(tài)的模塊比如埋點(diǎn)、日志。兩者各司其職代碼反而更清晰。8. 我個(gè)人的實(shí)操心得與避坑清單用了幾年 EventBus踩過的坑基本都在這了。最后分享幾條血淚經(jīng)驗(yàn)都是文檔里不會(huì)寫的。第一條事件類一定要加 const 構(gòu)造函數(shù)。這不是性能優(yōu)化是習(xí)慣問題。const 能讓你在 fire 的時(shí)候直接const XxxEvent()代碼更干凈而且編譯期就能發(fā)現(xiàn)類型錯(cuò)誤。第二條訂閱回調(diào)里第一行永遠(yuǎn)寫if (!mounted) return;。別問為什么問就是被setState called after dispose折磨過。這個(gè)判斷成本極低但能擋掉 90% 的偶發(fā)崩潰。第三條不要在事件里傳 BuildContext。Context 的生命周期和頁面綁定事件是全局的傳 Context 出去極容易導(dǎo)致 use after dispose。如果回調(diào)里需要 Context讓訂閱者自己從自己的 State 里拿。第四條事件數(shù)量控制在合理范圍。我見過一個(gè)項(xiàng)目定義了上百個(gè)事件類最后沒人搞得清哪個(gè)事件對(duì)應(yīng)哪個(gè)邏輯。我的建議是單個(gè)模塊的事件不超過 10 個(gè)超過就說明該用狀態(tài)管理了。第五條寫單元測(cè)試時(shí) mock 掉 EventBus。因?yàn)?EventBus 是全局單例測(cè)試之間會(huì)互相污染。我的做法是在測(cè)試的 setUp 里重建單例或者用依賴注入把 EventBus 傳進(jìn)去測(cè)試時(shí)傳一個(gè)干凈的實(shí)例。這個(gè)內(nèi)容后續(xù)還可以這樣擴(kuò)展一是把 EventBus 和 Flutter 的Isolate結(jié)合做跨線程事件通信二是做一個(gè)可視化的事件流監(jiān)控面板開發(fā)時(shí)實(shí)時(shí)查看事件分發(fā)情況三是研究event_bus包的替代實(shí)現(xiàn)比如基于ValueNotifier自己擼一個(gè)更輕量的版本。這些方向我都試過一部分有機(jī)會(huì)再單獨(dú)開一篇聊。