1. 程式人生 > >flutter_bloc使用解析---騷年,你還在手搭bloc嗎!

flutter_bloc使用解析---騷年,你還在手搭bloc嗎!

## 前言 - 首先,有很多的文章在說flutter bloc模式的應用,但是百分之八九十的文章都是在說,使用StreamController+StreamBuilder搭建bloc,提升效能的會加上InheritedWidget,這些文章看了很多,真正寫使用bloc作者開發的flutter_bloc卻少之又少。沒辦法,只能去bloc的github上去找使用方式,最後去bloc官網翻文件。 - 蛋痛,各位叼毛,就不能好好說說flutter_bloc的使用嗎?非要各種抄bloc模式提出作者的那倆篇文章。現在,搞的雜家這個伸手黨要自己去翻文件總結(手動滑稽)。 ![表情1](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200804152051.png) **專案效果(建議PC瀏覽器開啟)** - [Bloc範例效果](http://cnad666.gitee.io/book_web_manage) - [Cubit範例效果](https://cnad666.gitee.io/flutter_use/#/) **下面是Flutter_Bloc歷程的一系列連結** - [Flutter_Bloc起源](https://www.didierboelens.com/2018/08/reactive-programming-streams-bloc/) - [Flutter_Bloc模式優化](https://www.didierboelens.com/2018/12/reactive-programming-streams-bloc-practical-use-cases/) - [Flutter_Bloc誕生](https://medium.com/flutter-community/flutter-bloc-package-295b53e95c5c) - [Flutter_Bloc官網文件](https://bloclibrary.dev/#/) 前面三個,是bloc作者寫的bloc模式文件,典型的觀察者模式的應用,最原始的就是java中CallBack形式。前倆篇文章就是咱們這些大抄子的主要“參考”的資料來源,這三篇文章在掘金上有翻譯版,搜下bloc就能找到。最後一篇文章就是我主要總結歸納的源泉,作者在官網上寫了好幾個demo:計時器,登入,Todos,天氣等等,大家可以自己去看看。 ### 問題 初次使用flutter_bloc框架,可能會有幾個疑問 - state裡面定義了太多變數,某個事件只需要更新其中一個變數,其它的變數賦相同值麻煩 - 進入某個模組,進行初始化操作:複雜的邏輯運算,網路請求等,入口在哪定義 ## 效果 - 好了,嗶嗶了一堆,看下咱們要用flutter_bloc實現的效果。 ![bloc演示](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200804152120.gif) - 直接開Chrome演示,大家在虛擬機器上跑也一樣。 ## 引用 - 先說明下,bloc給的api很多,不同的api針對與解決場景不同,我要是把官網那些api全抄過也沒啥意義;不,也有可能可以裝幣,我要是不說明,大家說不定以為是我自己總結的呢!哈哈。 - OK,大家要是想知道全場景的使用,可以去官網翻翻文件,我覺得學習一個模式或者框架的時候,最主要的是把主流程跑通,起碼可以符合標準的堆頁面,這樣的話,就可以把這玩意用起來,再遇到想要的什麼細節,就可以自己去翻文件,畢竟大體上已經懂了,寫過了幾個頁面,也有些體會,再去翻文件就很快能理解了。 ### 庫 ```dart flutter_bloc: ^6.0.6 #狀態管理框架 equatable: ^1.2.3 #增強元件相等性判斷 ``` - 看看flutter_bloc都推到6.0了,別再用StreamController手搭Bloc了! ### 外掛 在Android Studio設定的Plugins裡,搜尋:Bloc ![外掛搜尋](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200804152204.png) 安裝重啟下,就OK了 - 右擊相應的資料夾,選擇“Bloc Class”,我在main資料夾新建的,填入的名字:main,就自動生成下面三個檔案;:main_bloc,main_event,main_state;main_view是我自己新建,用來寫頁面的。 ![新建bloc檔案](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200804152227.png) ![目錄結構新建bloc檔案](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200804152237.png) - 是不是覺得,還在手動新建這些bloc檔案low爆了;就好像fish_redux,不用外掛,讓我手動去建立那六個檔案,寫那些模板程式碼,真的要原地爆炸。 ## Bloc範例 ### 初始化程式碼 來看下這三個生成的bloc檔案:main_bloc,main_event,main_state - main_bloc:這裡就是咱們主要寫邏輯的頁面了 - mapEventToState方法只有一個引數,後面自動帶了一個逗號,格式化程式碼就分三行了,建議刪掉逗號,格式化程式碼。 ```dart class MainBloc extends Bloc { MainBloc() : super(MainInitial()); @override Stream mapEventToState( MainEvent event, ) async* { // TODO: implement mapEventToState } } ``` - main_event:這裡是執行的各類事件,有點類似fish_redux的action層 ```dart @immutable abstract class MainEvent {} ``` - main_state:狀態資料放在這裡儲存,中轉 ```dart @immutable abstract class MainState {} class MainInitial extends MainState {} ``` ### 實現 - 說明 - 這裡對於簡單的頁面,state的使用抽象狀態繼承實現的方式,未免有點麻煩,這裡我進行一點小改動,state的實現類別有很多,官網寫demo也有不用抽象類,直接class,類似實體類的方式開搞的。 - 老夫在程式碼關鍵點寫上"///"型別註釋,大家仔細看看,拷進Android Studio裡面,這些地方會變綠!大家好好體會下綠色程式碼! - main_bloc - state變數是框架內部定義的,會預設儲存上一次同步的MainSate物件的值 ```dart class MainBloc extends Bloc { MainBloc() : super(MainState(selectedIndex: 0, isExtended: false)); @override Stream mapEventToState(MainEvent event) async* { ///main_view中新增的事件,會在此處回撥,此處處理完資料,將資料yield,BlocBuilder就會重新整理元件 if (event is SwitchTabEvent) { ///獲取到event事件傳遞過來的值,咱們拿到這值塞進MainState中 ///直接在state上改變內部的值,然後yield,只能觸發一次BlocBuilder,它內部會比較上次MainState物件,如果相同,就不build yield MainState() ..selectedIndex = event.selectedIndex ..isExtended = state.isExtended; } else if (event is IsExtendEvent) { yield MainState() ..selectedIndex = state.selectedIndex ..isExtended = !state.isExtended; } } } ``` - main_event:在這裡就能看見,view觸發了那些事件了;維護起來也很爽,看看這裡,也很快能懂頁面在幹嘛了 ```dart @immutable abstract class MainEvent extends Equatable{ const MainEvent(); } ///切換NavigationRail的tab class SwitchTabEvent extends MainEvent{ final int selectedIndex; const SwitchTabEvent({@required this.selectedIndex}); @override List get props => [selectedIndex]; } ///展開NavigationRail,這個邏輯比較簡單,就不用傳引數了 class IsExtendEvent extends MainEvent{ const IsExtendEvent(); @override List get props => []; } ``` - main_state:state有很多種寫法,在bloc官方文件上,不同專案state的寫法也很多 - 這邊變數名可以設定為私用,用get和set可選擇性的設定讀寫許可權,因為我這邊設定的倆個變數全是必用的,讀寫均要,就設定公有型別,不用下劃線“_”去標記私有了。 ```dart class MainState{ int selectedIndex; bool isExtended; MainState({this.selectedIndex, this.isExtended}); } ``` - 對於生成的模板程式碼,我們在這:去掉@immutable註解,去掉abstract; - 這裡說下加上@immutable和abstract的作用,這邊是為了標定不同狀態,拿很典型的列表資料載入說明,列表載入的時候一般有三種狀態 - 獲取資料前,列表的佈局展示空樣式:LoadingBeforeState - 獲取資料失敗,顯示出載入失敗的佈局,或提升重新載入的樣式提升:LoadingFailureState - 獲取資料成功,顯示出列表資料:LoadingSuccessState - 針對上面三種狀態,需要展示不同的佈局,這樣我們就可以繼承抽象的狀態類:LoadingState,針對不同狀態實現上面三種不同的狀態類,不同的狀態可以定義不同的引數,然後在view中去判斷呼叫 - 這種實現不同狀態,對不同狀態進行管理,有點設計模式中-狀態模式的味道 - 下面程式碼是對上述描述的一種程式碼展示,可以瞧瞧;跑demo的時候,這下面的程式碼就不用抄了,僅做演示 ```dart @immutable abstract class LoadingState extends Equatable {} class LoadingInitial extends LoadingState { @override List get props => []; } class LoadingBeforeSate extends LoadingState{ ///實現相應的欄位資訊 @override List get props => []; } class LoadingFailureState extends LoadingState{ ///實現相應的欄位資訊 @override List get props => []; } class LoadingSuccessState extends LoadingState{ ///實現相應的欄位資訊 @override List get props => []; } ///在View中使用,虛擬碼 BlocBuilder(builder: (context, state) { if(state is LoadingBeforeSate){ return Beforewidget(state.XX,..); } else if(state is LoadingFailureState){ return FailureWidget(state.XX,state.XX,...); } else if(state is LoadingSuccessState){ return SuccessWidget(state.XX); } else { return ErrorWidget(...); } }) ``` - main_view - 這邊就是咱們的介面層了,很簡單,將需要重新整理的元件,用BlocBuilder包裹起來,使用BlocBuilder:提供的state去賦值就ok了,context去新增執行的事件,context用StatelessWidget中提供的或者BlocBuilder提供的都行 ```dart void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: MainPage(), ); } } class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { ///建立BlocProvider的,表明該Page,我們是用MainBloc,MainBloc是屬於該頁面的Bloc了 return BlocProvider( create: (BuildContext context) => MainBloc(), child: BodyPage(), ); } } class BodyPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Bloc')), body: totalPage(), ); } } Widget totalPage() { return Row( children: [ navigationRailSide(), Expanded(child: Center( child: BlocBuilder(builder: (context, state) { ///看這看這:重新整理元件! return Text("selectedIndex:" + state.selectedIndex.toString()); }), )) ], ); } //增加NavigationRail元件為側邊欄 Widget navigationRailSide() { //頂部widget Widget topWidget = Center( child: Padding( padding: const EdgeInsets.all(8.0), child: Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( image: NetworkImage("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3383029432,2292503864&fm=26&gp=0.jpg"), fit: BoxFit.fill), )), ), ); //底部widget Widget bottomWidget = Container( child: BlocBuilder( builder: (context, state) { return FloatingActionButton( onPressed: () { ///新增NavigationRail展開,收縮事件 context.bloc().add(IsExtendEvent()); }, ///看這看這:重新整理元件! child: Icon(state.isExtended ? Icons.send : Icons.navigation), ); }, ), ); return BlocBuilder(builder: (context, state) { return NavigationRail( backgroundColor: Colors.white12, elevation: 3, ///看這看這:重新整理元件! extended: state.isExtended, labelType: state.isExtended ? NavigationRailLabelType.none : NavigationRailLabelType.selected, //側邊欄中的item destinations: [ NavigationRailDestination( icon: Icon(Icons.add_to_queue), selectedIcon: Icon(Icons.add_to_photos), label: Text("測試一")), NavigationRailDestination( icon: Icon(Icons.add_circle_outline), selectedIcon: Icon(Icons.add_circle), label: Text("測試二")), NavigationRailDestination( icon: Icon(Icons.bubble_chart), selectedIcon: Icon(Icons.broken_image), label: Text("測試三")), ], //頂部widget leading: topWidget, //底部widget trailing: bottomWidget, selectedIndex: state.selectedIndex, onDestinationSelected: (int index) { ///新增切換tab事件 context.bloc().add(SwitchTabEvent(selectedIndex: index)); }, ); }); } ``` ## Bloc範例優化 ### 反思 從上面的程式碼來看,實際存在幾個隱式問題,這些問題,剛開始使用時候,沒異常的感覺,但是使用bloc久了後,感覺肯定越來越強烈 - state問題 - 初始化問題:這邊初始化是在bloc裡,直接在構造方法裡面賦初值的,state中一旦變數多了,還是這麼寫,會感覺極其難受,不好管理。需要優化 - 可以看見這邊我們只改動selectedIndex或者isExtended;另一個變數不需要變動,需要保持上一次的資料,進行了此類:state.selectedIndex或者state.isExtended賦值,一旦變數達到十幾個乃至幾十個,還是如此寫,是讓人極其崩潰的。需要優化 - bloc問題 - 如果進行一個頁面,需要進行復雜的運算或者請求介面後,才能知曉資料,進行賦值,這裡肯定需要一個初始化入口,初始化入口需要怎樣去定義呢? ### 優化實現 這邊完整走一下流程,讓大家能有個完整的思路 - state:首先來看看我們對state中的優化,這邊進行了倆個很重要優化,增加倆個方法:init()和clone() - init():這裡初始化統一用init()方法去管理 - clone():這邊克隆方法,是非常重要的,一旦變數達到倆位數以上,就能深刻體會該方法是多麼的重要 ```dart class MainState { int selectedIndex; bool isExtended; ///初始化方法,基礎變數也需要賦初值,不然會報空異常 MainState init() { return MainState() ..selectedIndex = 0 ..isExtended = false; } ///clone方法,此方法實現參考fish_redux的clone方法 ///也是對官方Flutter Login Tutorial這個demo中copyWith方法的一個優化 ///Flutter Login Tutorial(https://bloclibrary.dev/#/flutterlogintutorial) MainState clone() { return MainState() ..selectedIndex = selectedIndex ..isExtended = isExtended; } } ``` - event - 這邊定義一個MainInit()初始化方法,同時去掉Equatable繼承,在我目前的使用中,感覺它用處不大。。。 ```dart @immutable abstract class MainEvent {} ///初始化事件,這邊目前不需要傳什麼值 class MainInitEvent extends MainEvent {} ///切換NavigationRail的tab class SwitchTabEvent extends MainEvent { final int selectedIndex; SwitchTabEvent({@required this.selectedIndex}); } ///展開NavigationRail,這個邏輯比較簡單,就不用傳引數了 class IsExtendEvent extends MainEvent {} ``` - bloc - 這增加了初始化方法,請注意,如果需要進行非同步請求,同時需要將相關邏輯提煉一個方法,咱們在這裡配套Future和await就能解決在非同步場景下同步資料問題 - 這裡使用了克隆方法,可以發現,我們只要關注自己需要改變的變數就行了,其它的變數都在內部賦值好了,我們不需要去關注;這就大大的便捷了頁面中有很多變數,只需要變動一倆個變數的場景 - 注意:如果變數的資料未改變,介面相關的widget是不會重繪的;只會重繪變數被改變的widget ```dart class MainBloc extends Bloc { MainBloc() : super(MainState().init()); @override Stream mapEventToState(MainEvent event) async* { ///main_view中新增的事件,會在此處回撥,此處處理完資料,將資料yield,BlocBuilder就會重新整理元件 if (event is MainInitEvent) { yield await init(); } else if (event is SwitchTabEvent) { ///獲取到event事件傳遞過來的值,咱們拿到這值塞進MainState中 ///直接在state上改變內部的值,然後yield,只能觸發一次BlocBuilder,它內部會比較上次MainState物件,如果相同,就不build yield switchTap(event); } else if (event is IsExtendEvent) { yield isExtend(); } } ///初始化操作,在網路請求的情況下,需要使用如此方法同步資料 Future init() async { return state.clone(); } ///切換tab MainState switchTap(SwitchTabEvent event) { return state.clone()..selectedIndex = event.selectedIndex; } ///是否展開 MainState isExtend() { return state.clone()..isExtended = !state.isExtended; } } ``` - view - view層程式碼太多,這邊只增加了個初始化事件,就不重新把全部程式碼貼出來了,初始化操作直接在建立的時候,在XxxBloc上使用add()方法就行了,就能起到進入頁面,初始化一次的效果;add()方法也是Bloc類中提供的,遍歷事件的時候,就特地檢查了add()這個方法是否添加了事件;說明,這是框架特地提供了一個初始化的方法 - 這個初始化方式是在官方示例找到的 - 專案名:Flutter Infinite List Tutorial - 專案地址:[flutter-infinite-list-tutorial](https://bloclibrary.dev/#/flutterinfinitelisttutorial?id=flutter-infinite-list-tutorial) ```dart class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( ///在MainBloc上使用add方法,新增初始化事件 create: (BuildContext context) => MainBloc()..add(MainInitEvent()), child: BodyPage(), ); } } ///下方其餘程式碼省略........... ``` ### 搞定 - OK,經過這樣的優化,解決了幾個痛點。實際在view中反覆是要用BlocBuilder去更新view,寫起來有點麻煩,這裡我們可以寫一個,將其中state和context變數,往提出來的Widget方法傳值,也是蠻不錯的 - 大家保持觀察者模式的思想就行了;觀察者(回撥重新整理控制元件)和被觀察者(產生相應事件,新增事件,去通知觀察者),bloc層是處於觀察者和被觀察者中間的一層,我們可以在bloc裡面搞業務,搞邏輯,搞網路請求,不能搞基;拿到Event事件傳遞過來的資料,把處理好的、符合要求的資料返回給view層的觀察者就行了。 - 使用框架,不拘泥框架,在觀察者模式的思想上,靈活的去使用flutter_bloc提供Api,這樣可以大大的縮短我們的開發時間! ## Cubit範例 - Cubit是Bloc模式的一種簡化版,去掉了event這一層,對於簡單的頁面,用Cubit來實現,開發體驗是大大的好啊,下面介紹下該種模式的寫法 ### 建立 - 首先建立Cubit一組檔案,選擇“Cubit Class”,點選,新建名稱填寫:Counter ![image-20201010155420462](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20201010165016.png) 新建好後,他會生成倆個檔案:counter_cubit,counter_state,來看下生成的程式碼 #### 原始生成程式碼 - counter_cubit ```dart class CounterCubit extends Cubit { CounterCubit() : super(CounterInitial()); } ``` - counter_state ```dart @immutable abstract class CounterState {} class CounterInitial extends CounterState {} ``` 按照生成的這種state方式去寫,比較麻煩,這邊調整下 #### 調整後代碼 - counter_cubit ```dart class CounterCubit extends Cubit { CounterCubit() : super(CounterState().init()); } ``` - counter_state ```dart class CounterState { ///初始化方法 CounterState init() { return CounterState(); } ///克隆方法,針對於重新整理介面資料 CounterState clone() { return CounterState(); } } ``` OK,這樣調整了下,下面寫起來就會舒服很多,也會很省事 ### 實現計時器 - 來實現下一個灰常簡單的計數器 #### 效果 - 來看下實現效果吧,這邊不上圖了,大家點選下面的連結,可以直接體驗Cubit模式寫的計時器 - 實現效果:[點我體驗實際效果](https://cnad666.gitee.io/flutter_use/#/counter) #### 實現 實現很簡單,三個檔案就搞定,看下流程:state -> cubit -> view - state:這個很簡單,加個計時變數 ```dart class CounterState { int count; CounterState init() { return CounterState()..count = 0; } CounterState clone() { return CounterState()..count = count; } } ``` - cubit - 這邊加了個自增方法:increase() - event層實際是所有行為的一種整合,方便對邏輯過於複雜的頁面,所有行為的一種維護;但是過於簡單的頁面,就那麼幾個事件,還單獨維護,就沒什麼必要了 - 在cubit層寫的公共方法,在view裡面能直接呼叫,更新資料使用:emit() - cubit層應該可以算是:bloc層和event層一種結合後的簡寫 ```dart class CounterCubit extends Cubit { CounterCubit() : super(CounterState().init()); ///自增 void increase() => emit(state.clone()..count = ++state.count); } ``` - view - view層的程式碼就非常簡單了,點選方法裡面呼叫cubit層的自增方法就ok了 ```dart class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => CounterCubit(), child: BlocBuilder(builder: _counter), ); } Widget _counter(BuildContext context, CounterState state) { return Scaffold( appBar: AppBar(title: const Text('Cubit範例')), body: Center( child: Text('點選了 ${state.count} 次', style: TextStyle(fontSize: 30.0)), ), floatingActionButton: FloatingActionButton( onPressed: () => context.bloc().increase(), child: const Icon(Icons.add), ), ); } } ``` ### 總結 在Bloc模式裡面,如果頁面不是過於複雜,使用Cubit去寫,基本完全夠用了;但是如果業務過於複雜,還是需要用Bloc去寫,需要將所有的事件行為管理起來,便於後期維護 OK,Bloc的簡化模組,Cubit模式就這樣講完了,對於自己業務寫的小專案,我就經常用這個Cubit去寫 ## 最後 - Bloc還有很多Api針對不同的場景非常的實用,例如:MultiBlocProvider,BlocListener,MultiBlocListener,BlocConsumer等等,這裡面有些Api和Provider的Api是非常相似的,例如MultiXxxxx,這都是為了減少巢狀,提供多個全域性Bloc而提供,大家可以去瞧瞧看,用法也都非常的相似 - Cubit範例程式碼地址 - [Cubit範例程式碼](https://github.com/CNAD666/ExampleCode/tree/master/Flutter/flutter_use) - Bloc範例程式碼地址 - [Bloc範例程式碼](https://github.com/CNAD666/book_manage) - flutter_bloc相關Api白嫖地址 - [flutter_bloc相關Api](https://bloclibrary.dev/#/flutterbloccoreconcepts?id=bloc-widgets) - flutter_bloc - GitHub:[https://github.com/felangel/bloc](https://github.com/felangel/bloc) - Pub:[https://pub.dev/packages/flutter_bloc](https://pub.dev/packages/flutter_bloc)