前言
-
首先,有很多的文章在說flutter bloc模式的應用,但是百分之八九十的文章都是在說,使用StreamController+StreamBuilder搭建bloc,提升效能的會加上InheritedWidget,這些文章看了很多,真正寫使用bloc作者開發的flutter_bloc卻少之又少。沒辦法,只能去bloc的github上去找使用方式,最後去bloc官網翻文件。
-
蛋痛,各位叼毛,就不能好好說說flutter_bloc的使用嗎?非要各種抄bloc模式提出作者的那倆篇文章。現在,搞的雜家這個伸手黨要自己去翻文件總結(手動滑稽)。
專案效果(建議PC瀏覽器開啟)
下面是Flutter_Bloc歷程的一系列連結
-
前面三個,是bloc作者寫的bloc模式文件,典型的觀察者模式的應用,最原始的就是java中CallBack形式。前倆篇文章就是我們們這些大抄子的主要“參考”的資料來源,這三篇文章在掘金上有翻譯版,搜下bloc就能找到。最後一篇文章就是我主要總結歸納的源泉,作者在官網上寫了好幾個demo:計時器,登入,Todos,天氣等等,大家可以自己去看看。
問題
初次使用flutter_bloc框架,可能會有幾個疑問
- state裡面定義了太多變數,某個事件只需要更新其中一個變數,其它的變數賦相同值麻煩
- 進入某個模組,進行初始化操作:複雜的邏輯運算,網路請求等,入口在哪定義
效果
- 好了,嗶嗶了一堆,看下我們們要用flutter_bloc實現的效果。
- 直接開Chrome演示,大家在虛擬機器上跑也一樣。
引用
-
先說明下,bloc給的api很多,不同的api針對與解決場景不同,我要是把官網那些api全抄過也沒啥意義;不,也有可能可以裝幣,我要是不說明,大家說不定以為是我自己總結的呢!哈哈。
-
OK,大家要是想知道全場景的使用,可以去官網翻翻文件,我覺得學習一個模式或者框架的時候,最主要的是把主流程跑通,起碼可以符合標準的堆頁面,這樣的話,就可以把這玩意用起來,再遇到想要的什麼細節,就可以自己去翻文件,畢竟大體上已經懂了,寫過了幾個頁面,也有些體會,再去翻文件就很快能理解了。
庫
flutter_bloc: ^6.0.6 #狀態管理框架
equatable: ^1.2.3 #增強元件相等性判斷
- 看看flutter_bloc都推到6.0了,別再用StreamController手搭Bloc了!
外掛
在Android Studio設定的Plugins裡,搜尋:Bloc
安裝重啟下,就OK了
- 右擊相應的資料夾,選擇“Bloc Class”,我在main資料夾新建的,填入的名字:main,就自動生成下面三個檔案;:main_bloc,main_event,main_state;main_view是我自己新建,用來寫頁面的。
- 是不是覺得,還在手動新建這些bloc檔案low爆了;就好像fish_redux,不用外掛,讓我手動去建立那六個檔案,寫那些模板程式碼,真的要原地爆炸。
Bloc範例
初始化程式碼
來看下這三個生成的bloc檔案:main_bloc,main_event,main_state
- main_bloc:這裡就是我們們主要寫邏輯的頁面了
- mapEventToState方法只有一個引數,後面自動帶了一個逗號,格式化程式碼就分三行了,建議刪掉逗號,格式化程式碼。
class MainBloc extends Bloc<MainEvent, MainState> {
MainBloc() : super(MainInitial());
@override
Stream<MainState> mapEventToState(
MainEvent event,
) async* {
// TODO: implement mapEventToState
}
}
- main_event:這裡是執行的各類事件,有點類似fish_redux的action層
@immutable
abstract class MainEvent {}
- main_state:狀態資料放在這裡儲存,中轉
@immutable
abstract class MainState {}
class MainInitial extends MainState {}
實現
- 說明
- 這裡對於簡單的頁面,state的使用抽象狀態繼承實現的方式,未免有點麻煩,這裡我進行一點小改動,state的實現類別有很多,官網寫demo也有不用抽象類,直接class,類似實體類的方式開搞的。
- 老夫在程式碼關鍵點寫上"///"型別註釋,大家仔細看看,拷進Android Studio裡面,這些地方會變綠!大家好好體會下綠色程式碼!
- main_bloc
- state變數是框架內部定義的,會預設儲存上一次同步的MainSate物件的值
class MainBloc extends Bloc<MainEvent, MainState> {
MainBloc() : super(MainState(selectedIndex: 0, isExtended: false));
@override
Stream<MainState> 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觸發了那些事件了;維護起來也很爽,看看這裡,也很快能懂頁面在幹嘛了
@immutable
abstract class MainEvent extends Equatable{
const MainEvent();
}
///切換NavigationRail的tab
class SwitchTabEvent extends MainEvent{
final int selectedIndex;
const SwitchTabEvent({@required this.selectedIndex});
@override
List<Object> get props => [selectedIndex];
}
///展開NavigationRail,這個邏輯比較簡單,就不用傳引數了
class IsExtendEvent extends MainEvent{
const IsExtendEvent();
@override
List<Object> get props => [];
}
- main_state:state有很多種寫法,在bloc官方文件上,不同專案state的寫法也很多
- 這邊變數名可以設定為私用,用get和set可選擇性的設定讀寫許可權,因為我這邊設定的倆個變數全是必用的,讀寫均要,就設定公有型別,不用下劃線“_”去標記私有了。
class MainState{
int selectedIndex;
bool isExtended;
MainState({this.selectedIndex, this.isExtended});
}
- 對於生成的模板程式碼,我們在這:去掉@immutable註解,去掉abstract;
- 這裡說下加上@immutable和abstract的作用,這邊是為了標定不同狀態,拿很典型的列表資料載入說明,列表載入的時候一般有三種狀態
- 獲取資料前,列表的佈局展示空樣式:LoadingBeforeState
- 獲取資料失敗,顯示出載入失敗的佈局,或提升重新載入的樣式提升:LoadingFailureState
- 獲取資料成功,顯示出列表資料:LoadingSuccessState
- 針對上面三種狀態,需要展示不同的佈局,這樣我們就可以繼承抽象的狀態類:LoadingState,針對不同狀態實現上面三種不同的狀態類,不同的狀態可以定義不同的引數,然後在view中去判斷呼叫
- 這種實現不同狀態,對不同狀態進行管理,有點設計模式中-狀態模式的味道
- 這裡說下加上@immutable和abstract的作用,這邊是為了標定不同狀態,拿很典型的列表資料載入說明,列表載入的時候一般有三種狀態
- 下面程式碼是對上述描述的一種程式碼展示,可以瞧瞧;跑demo的時候,這下面的程式碼就不用抄了,僅做演示
@immutable
abstract class LoadingState extends Equatable {}
class LoadingInitial extends LoadingState {
@override
List<Object> get props => [];
}
class LoadingBeforeSate extends LoadingState{
///實現相應的欄位資訊
@override
List<Object> get props => [];
}
class LoadingFailureState extends LoadingState{
///實現相應的欄位資訊
@override
List<Object> get props => [];
}
class LoadingSuccessState extends LoadingState{
///實現相應的欄位資訊
@override
List<Object> get props => [];
}
///在View中使用,虛擬碼
BlocBuilder<MainBloc, MainState>(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提供的都行
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<MainBloc, MainState>(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<MainBloc, MainState>(
builder: (context, state) {
return FloatingActionButton(
onPressed: () {
///新增NavigationRail展開,收縮事件
context.bloc<MainBloc>().add(IsExtendEvent());
},
///看這看這:重新整理元件!
child: Icon(state.isExtended ? Icons.send : Icons.navigation),
);
},
),
);
return BlocBuilder<MainBloc, MainState>(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<MainBloc>().add(SwitchTabEvent(selectedIndex: index));
},
);
});
}
Bloc範例優化
反思
從上面的程式碼來看,實際存在幾個隱式問題,這些問題,剛開始使用時候,沒異常的感覺,但是使用bloc久了後,感覺肯定越來越強烈
- state問題
- 初始化問題:這邊初始化是在bloc裡,直接在構造方法裡面賦初值的,state中一旦變數多了,還是這麼寫,會感覺極其難受,不好管理。需要優化
- 可以看見這邊我們只改動selectedIndex或者isExtended;另一個變數不需要變動,需要保持上一次的資料,進行了此類:state.selectedIndex或者state.isExtended賦值,一旦變數達到十幾個乃至幾十個,還是如此寫,是讓人極其崩潰的。需要優化
- bloc問題
- 如果進行一個頁面,需要進行復雜的運算或者請求介面後,才能知曉資料,進行賦值,這裡肯定需要一個初始化入口,初始化入口需要怎樣去定義呢?
優化實現
這邊完整走一下流程,讓大家能有個完整的思路
- state:首先來看看我們對state中的優化,這邊進行了倆個很重要優化,增加倆個方法:init()和clone()
- init():這裡初始化統一用init()方法去管理
- clone():這邊克隆方法,是非常重要的,一旦變數達到倆位數以上,就能深刻體會該方法是多麼的重要
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繼承,在我目前的使用中,感覺它用處不大。。。
@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
class MainBloc extends Bloc<MainEvent, MainState> {
MainBloc() : super(MainState().init());
@override
Stream<MainState> 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<MainState> 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
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
新建好後,他會生成倆個檔案:counter_cubit,counter_state,來看下生成的程式碼
原始生成程式碼
- counter_cubit
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterInitial());
}
- counter_state
@immutable
abstract class CounterState {}
class CounterInitial extends CounterState {}
按照生成的這種state方式去寫,比較麻煩,這邊調整下
調整後程式碼
- counter_cubit
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterState().init());
}
- counter_state
class CounterState {
///初始化方法
CounterState init() {
return CounterState();
}
///克隆方法,針對於重新整理介面資料
CounterState clone() {
return CounterState();
}
}
OK,這樣調整了下,下面寫起來就會舒服很多,也會很省事
實現計時器
- 來實現下一個灰常簡單的計數器
效果
- 來看下實現效果吧,這邊不上圖了,大家點選下面的連結,可以直接體驗Cubit模式寫的計時器
- 實現效果:點我體驗實際效果
實現
實現很簡單,三個檔案就搞定,看下流程:state -> cubit -> view
- state:這個很簡單,加個計時變數
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層一種結合後的簡寫
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterState().init());
///自增
void increase() => emit(state.clone()..count = ++state.count);
}
- view
- view層的程式碼就非常簡單了,點選方法裡面呼叫cubit層的自增方法就ok了
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => CounterCubit(),
child: BlocBuilder<CounterCubit, CounterState>(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<CounterCubit>().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範例程式碼地址
- Bloc範例程式碼地址
- flutter_bloc相關Api白嫖地址
- flutter_bloc