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


flutter_bloc使用將從下圖的三個維度說明

flutter_bloc

前言

  • 首先,有很多的文章在說flutter bloc模式的應用,但是百分之八九十的文章都是在說,使用StreamController+StreamBuilder搭建bloc,提升性能的會加上InheritedWidget,這些文章看了很多,真正寫使用bloc作者開發的flutter_bloc卻少之又少。沒辦法,只能去bloc的github上去找使用方式,最后去bloc官網翻文檔。

  • 蛋痛,各位叼毛,就不能好好說說flutter_bloc的使用嗎?非要各種抄bloc模式提出作者的那倆篇文章。現在,搞的雜家這個伸手黨要自己去翻文檔總結(手動滑稽)。

表情1

項目效果(建議PC瀏覽器打開)

問題

初次使用flutter_bloc框架,可能會有幾個疑問

  • state里面定義了太多變量,某個事件只需要更新其中一個變量,其它的變量賦相同值麻煩
  • 進入某個模塊,進行初始化操作:復雜的邏輯運算,網絡請求等,入口在哪定義

准備工作

說明

  • 這里說明下,文章里把BlocBuilder放在頂層,因為本身頁面非常簡單,也是為了更好呈現頁面結構,所以才放在頂層,如果需要更加顆粒化控件更新區域,請將BlocBuilder包裹你需要更新的控件區域即可

引用

  • 我覺得學習一個模式或者框架的時候,最主要的是把主流程跑通,起碼可以符合標准的堆頁面,這樣的話,就可以把這玩意用起來,再遇到想要的什么細節,就可以自己去翻文檔,畢竟大體上已經懂了,寫過了幾個頁面,也有些體會,再去翻文檔就很快能理解了
  • 實際上Bloc給的API也不多,就幾個API,相關API使用說明都寫在文章最后

flutter_bloc: ^6.1.1 #狀態管理框架
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文件

目錄結構新建bloc文件

  • 是不是覺得,還在手動新建這些bloc文件low爆了;就好像fish_redux,不用插件,讓我手動去創建那六個文件,寫那些模板代碼,真的要原地爆炸。

Bloc范例

效果

  • 好了,嗶嗶了一堆,看下咱們要用flutter_bloc實現的效果。

bloc演示

  • 直接開Chrome演示,大家在虛擬機上跑也一樣。

初始化代碼

來看下這三個生成的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 {}

實現

  • 主入口
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MainPage(),
    );
  }
}
  • 說明
    • 這里對於簡單的頁面,state的使用抽象狀態繼承實現的方式,未免有點麻煩,這里我進行一點小改動,state的實現類別有很多,官網寫demo也有不用抽象類,直接class,類似實體類的方式開搞的。
    • 相關代碼的注釋寫的比較多,大家可以着重看看
  • 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可選擇性的設置讀寫權限,因為我這邊設置的倆個變量全是必用的,讀寫均要,就設置公有類型,不用下划線“_”去標記私有了。

    • 對於生成的模板代碼,我們在這:去掉@immutable注解,去掉abstract;

    • 這里說下加上@immutable和abstract的作用,這邊是為了標定不同狀態,這種寫法,會使得代碼變得更加麻煩,用state不同狀態去標定業務事件,代價太大,這邊用一個變量去標定,很容易輕松代替

class MainState{
   int selectedIndex;
   bool isExtended;
  
   MainState({this.selectedIndex, this.isExtended});
}
  • main_view
    • 這邊就是咱們的界面層了,很簡單,將需要刷新的組件,用BlocBuilder包裹起來,使用BlocBuilder:提供的state去賦值就ok了,context去添加執行的事件,context用StatelessWidget中提供的或者BlocBuilder提供的都行
class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      //側邊欄
      _buildLeftNavigation(),

      //右邊主體內容
      Expanded(child: Center(
        child: BlocBuilder<MainBloc, MainState>(builder: (context, state) {
          return Text(
            "選擇Index:" + state.selectedIndex.toString(),
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ))
    ]);
  }

  Widget _buildBg({List<Widget> children}) {
    ///創建BlocProvider的,表明該Page,我們是用MainBloc,MainBloc是屬於該頁面的Bloc了
    return BlocProvider(
      create: (BuildContext context) => MainBloc(),
      child: Scaffold(
        appBar: AppBar(title: Text('Bloc')),
        body: Row(children: children),
      ),
    );
  }

  //增加NavigationRail組件為側邊欄
  Widget _buildLeftNavigation() {
    return BlocBuilder<MainBloc, MainState>(builder: (context, state) {
      return NavigationRail(
        backgroundColor: Colors.white,
        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: _buildNavigationTop(),
        //底部widget
        trailing: _buildNavigationBottom(),
        selectedIndex: state.selectedIndex,
        onDestinationSelected: (int index) {
          ///添加切換tab事件
          BlocProvider.of<MainBloc>(context)
              .add(SwitchTabEvent(selectedIndex: index));
        },
      );
    });
  }

  Widget _buildNavigationTop() {
    return 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 _buildNavigationBottom() {
    return Container(
      child: BlocBuilder<MainBloc, MainState>(
        builder: (context, state) {
          return FloatingActionButton(
            onPressed: () {
              ///添加NavigationRail展開,收縮事件
              BlocProvider.of<MainBloc>(context).add(IsExtendEvent());
            },
            child: Icon(state.isExtended ? Icons.send : Icons.navigation),
          );
        },
      ),
    );
  }
}

Bloc范例優化

反思

從上面的代碼來看,實際存在幾個隱式問題,這些問題,剛開始使用時候,沒異常的感覺,但是使用bloc久了后,感覺肯定越來越強烈

  • state問題
    • 初始化問題:這邊初始化是在bloc里,直接在構造方法里面賦初值的,state中一旦變量多了,還是這么寫,會感覺極其難受,不好管理。需要優化
    • 可以看見這邊我們只改動selectedIndex或者isExtended;另一個變量不需要變動,需要保持上一次的數據,進行了此類:state.selectedIndex或者state.isExtended賦值,一旦變量達到十幾個乃至幾十個,還是如此寫,是讓人極其崩潰的。需要優化
  • bloc問題
    • 如果進行一個頁面,需要進行復雜的運算或者請求接口后,才能知曉數據,進行賦值,這里肯定需要一個初始化入口,初始化入口需要怎樣去定義呢?

插件

因為官方插件生成的寫法,和調整后寫法差距有點大,而且官方插件不支持生成view層和相關設置,此處我就擼了一個插件,完善了相關功能

請注意,wrap代碼和提示代碼片段,參靠了官方插件規則

Wrap Widget 規則來着:intellij_generator_plugin

快捷代碼生成規則來着: intellij_generator_plugin

  • 在Android Studio里面搜索 flutter bloc

image-20210612163803311

  • 生成模板代碼

bloc

  • 支持修改后綴

image-20210612165242515

  • Wrap Widget (alt + enter):RepositoryProvider,BlocConsumer,BlocBuilder,BlocProvider,BlocListener

image-20210612164717855

  • 輸入 bloc 可生成快捷代碼片段

image-20210612164905296

優化實現

這邊完整走一下流程,讓大家能有個完整的思路

  • 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()這個方法是否添加了事件;說明,這是框架特地提供了一個初始化的方法
    • 這個初始化方式是在官方示例找到的
class MainPage extends StatelessWidget {
  ...
      
   Widget _buildBg({List<Widget> children}) {
    ///創建BlocProvider的,表明該Page,我們是用MainBloc,MainBloc是屬於該頁面的Bloc了
    return BlocProvider(
      create: (BuildContext context) => MainBloc()..add(MainInitEvent()),
      child: Scaffold(
        appBar: AppBar(title: Text('Bloc')),
        body: Row(children: children),
      ),
    );
  }
    
  ///下方其余代碼省略...........
}

搞定

  • OK,經過這樣的優化,解決了幾個痛點。實際在view中反復是要用BlocBuilder去更新view,寫起來有點麻煩,這里我們可以寫一個,將其中state和context變量,往提出來的Widget方法傳值,也是蠻不錯的
  • 大家保持觀察者模式的思想就行了;觀察者(回調刷新控件)和被觀察者(產生相應事件,添加事件,去通知觀察者),bloc層是處於觀察者和被觀察者中間的一層,我們可以在bloc里面搞業務,搞邏輯,搞網絡請求,不能搞基;拿到Event事件傳遞過來的數據,把處理好的、符合要求的數據返回給view層的觀察者就行了。
  • 使用框架,不拘泥框架,在觀察者模式的思想上,靈活的去使用flutter_bloc提供Api,這樣可以大大的縮短我們的開發時間!

Cubit范例

  • Cubit是Bloc模式的一種簡化版,去掉了event這一層,對於簡單的頁面,用Cubit來實現,開發體驗是大大的好啊,下面介紹下該種模式的寫法

創建

  • 首先創建Cubit一組文件,選擇“Cubit”,新建名稱填寫:Counter

image-20210612170053602

新建好后,他會生成三個文件:cubit,state,view;來看下生成的代碼

模板代碼

  • counter_cubit
class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(CounterState().init());
}
  • state
class CounterState {
  CounterState init() {
    return CounterState();
  }

  CounterState clone() {
    return CounterState();
  }
}
  • view
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => CounterCubit(),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final cubit = BlocProvider.of<CounterCubit>(context);

    return Container();
  }
}

實現計時器

  • 來實現下一個灰常簡單的計數器

效果

  • 來看下實現效果吧,這邊不上圖了,大家點擊下面的鏈接,可以直接體驗Cubit模式寫的計時器
  • 實現效果:點我體驗實際效果

實現

實現很簡單,三個文件就搞定,看下流程:state -> cubit -> view

  • state:這個很簡單,加個計時變量
class BlCubitCounterState {
  late int count;

  BlCubitCounterState init() {
    return BlCubitCounterState()..count = 0;
  }

  BlCubitCounterState clone() {
    return BlCubitCounterState()..count = count;
  }
}
  • cubit
    • 這邊加了個自增方法:increase()
    • event層實際是所有行為的一種整合,方便對邏輯過於復雜的頁面,所有行為的一種維護;但是過於簡單的頁面,就那么幾個事件,還單獨維護,就沒什么必要了
    • 在cubit層寫的公共方法,在view里面能直接調用,更新數據使用:emit()
    • cubit層應該可以算是:bloc層和event層一種結合后的簡寫
class BlCubitCounterCubit extends Cubit<BlCubitCounterState> {
  BlCubitCounterCubit() : super(BlCubitCounterState().init());

  ///自增
  void increment() => emit(state.clone()..count = ++state.count);
}
  • view
    • view層的代碼就非常簡單了,點擊方法里面調用cubit層的自增方法就ok了
class BlCubitCounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => BlCubitCounterCubit(),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final cubit = BlocProvider.of<BlCubitCounterCubit>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Bloc-Cubit范例')),
      body: Center(
        child: BlocBuilder<BlCubitCounterCubit, BlCubitCounterState>(
          builder: (context, state) {
            return Text(
              '點擊了 ${cubit.state.count} 次',
              style: TextStyle(fontSize: 30.0),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => cubit.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

總結

在Bloc模式里面,如果頁面不是過於復雜,使用Cubit去寫,基本完全夠用了;但是如果業務過於復雜,還是需要用Bloc去寫,需要將所有的事件行為管理起來,便於后期維護

OK,Bloc的簡化模塊,Cubit模式就這樣講完了,對於自己業務寫的小項目,我就經常用這個Cubit去寫

全局Bloc

說明

什么是全局Bloc?

  • BlocProvider介紹里面有這樣的形容:BlocProvider should be used to create new blocs which will be made available to the rest of the subtree(BlocProvider應該被用於創建新的Bloc,這些Bloc將可用於其子樹)
  • 這樣的話,我們只需要在主入口地方使用BlocProvider創建Bloc,就能使用全局的XxxBloc了,這里的全局XxxBloc,state狀態都會被保存的,除非關閉app,否則state里面的數據都不會被還原!
  • 注意:在主入口創建的XxxBloc,在主入口處創建了一次,在其它頁面均不需要再次創建,在任何頁面只需要使用BlocBuilder,便可以定點刷新及其獲取全局XxxBloc的state數據

使用場景

  • 全局的主題色,字體樣式和大小等等全局配置更改;這種情況,在需要全局屬性的地方,使用BlocBuilder對應的全局XxxBloc泛型去刷新數據就行了
  • 跨頁面去調用事件,既然是全局的XxxBloc,這就說明,我們可以在任何頁面,使用 BlocProvider.of<XxxBloc>(context)調用全局XxxBloc中事件,這就起到了一種跨頁面調用事件的效果
    • 使用全局Bloc做跨頁面事件時,應該明白,當你關閉Bloc對應的頁面,對應全局Bloc中的並不會被回收,下次進入頁面,頁面的數據還是上次退出頁面修改的數據,這里應該使用StatefulWidget,在initState生命周期處,初始化數據;或者在dispose生命周期處,還原數據源
    • 思考下:全局Bloc對象存在周期是在整個App存活周期,必然不能創建過多的全局Bloc,跨頁面傳遞事件使用全局Bloc應當只能做折中方案

效果圖

globalBloc

使用

來看下怎么創建和使用全局Bloc吧!

  • 主入口配置
    • 全局的Bloc創建還是蠻簡單的,這邊把MultiBlocProvider在Builder里面套在child上面就行了;當然了,把MultiBlocProvider套在MaterialApp上也是可以的
    • 這樣我們就獲得一個全局的SpanOneCubit
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MainPage(),
      builder: (BuildContext context, Widget child) {
        return MultiBlocProvider(
          providers: [
            ///此處通過BlocProvider創建的Bloc或者Cubit是全局的
            BlocProvider<SpanOneCubit>(
              create: (BuildContext context) => SpanOneCubit(),
            ),
          ],
          child: child,
        );
      },
    );
  }
}

需要用倆個Bloc模塊來演示,這里分別用SpanOneCubitSpanTwoCubit來演示,其中SpanOneCubit是全局的

SpanOneCubit

  • state
    • 先來看看State模塊的代碼,這里很簡單,只定義了count變量
class SpanOneState {
  int count;

  ///初始化方法
  SpanOneState init() {
    return SpanOneState()..count = 0;
  }

  ///克隆方法,針對於刷新界面數據
  SpanOneState clone() {
    return SpanOneState()..count = count;
  }
}
  • view
    • 這個頁面僅僅是展示計數變量的變化,因為在主入口使用了BlocProvider創建了SpanOneCubit,所以在這個頁面不需要再次創建,直接使用BlocBuilder便可以獲取其state
    • 可以發現,這個頁面使用了StatefulWidget,在initState周期中,初始化了數據源;這樣,每次進入頁面,數據源就不會保存為上一次改動的來,都會被初始化為我們想要的值;這個頁面能接受到任何頁面調用其事件,這樣就實現類似於廣播的一種效果(
class CubitSpanOnePage extends StatefulWidget {
  @override
  _SpanOnePageState createState() => _SpanOnePageState();
}

class _SpanOnePageState extends State<CubitSpanOnePage> {
  @override
  void initState() {
    BlocProvider.of<BlocSpanOneCubit>(context).init();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(title: Text('跨頁面-One')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => BlocProvider.of<BlocSpanOneCubit>(context).toSpanTwo(context),
        child: const Icon(Icons.arrow_forward_outlined),
      ),
      body: Center(
        child: BlocBuilder<BlocSpanOneCubit, BlocSpanOneState>(
          builder: (context, state) {
            return Text(
              'SpanTwoPage點擊了 ${state.count} 次',
              style: TextStyle(fontSize: 30.0),
            );
          },
        ),
      ),
    );
  }
}
  • cubit
    • cubit里面有三個事件,初始化,跳轉頁面,計數自增
class SpanOneCubit extends Cubit<SpanOneState> {
  SpanOneCubit() : super(SpanOneState().init());

  void init() {
    emit(state.init());
  }

  ///跳轉到跨頁面
  void toSpanTwo(BuildContext context) {
    Navigator.push(context, MaterialPageRoute(builder: (context) => SpanTwoPage()));
  }

  ///自增
  void increase() {
    state..count = ++state.count;
    emit(state.clone());
  }
}

SpanTwoCubit

  • state
    • 使用count,記錄下我們點擊自增的次數
class SpanTwoState {
  int count;

  ///初始化方法
  SpanTwoState init() {
    return SpanTwoState()..count = 0;
  }

  ///克隆方法,針對於刷新界面數據
  SpanTwoState clone() {
    return SpanTwoState()..count = count;
  }
}
  • view
    • 這地方我們需要創建使用BlocProvider一個SpanTwoCubit,這是使用Bloc的常規流程
    • 在自增的點擊事件里,我們調用本模塊和SpanOneCubit中的自增方法,OK,這里我們就能同步的改變SpanOneCubit模塊的數據了!
class CubitSpanTwoPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => BlocSpanTwoCubit()..init(context),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final cubit = BlocProvider.of<BlocSpanTwoCubit>(context);

    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(title: Text('跨頁面-Two')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          //改變SpanOneCubit模塊數據
          BlocProvider.of<BlocSpanOneCubit>(context).increase();

          //改變當前頁面數據
          cubit.increase();
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: BlocBuilder<BlocSpanTwoCubit, BlocSpanTwoState>(
          builder: (context, state) {
            return Text('當前點擊了 ${state.count} 次',
                style: TextStyle(fontSize: 30.0));
          },
        ),
      ),
    );
  }
}
  • cubit
    • 平平無奇的業務代碼
class SpanTwoCubit extends Cubit<SpanTwoState> {
  SpanTwoCubit() : super(SpanTwoState().init());

  void init(BuildContext context){
    emit(state.init());
  }

  ///自增
  void increase() => emit(state.clone()..count = ++state.count);
}

總結

OK,這樣便用全局Bloc實現了類似廣播的一種效果

  • 使用全局去刷新:主題,字體樣式和大小之類,每個頁面都要使用BlocBuilder對應的全局bloc去刷新對應的全局view模塊

Bloc API說明

BlocBuilder

BlocBuilder是Flutter窗口小部件,需要Blocbuilder函數。BlocBuilder處理構建小部件以響應新狀態。BlocBuilder與非常相似,StreamBuilder但具有更簡單的API,可以減少所需的樣板代碼量。該builder函數可能會被多次調用,並且應該是一個純函數,它會根據狀態返回小部件。

看看BlocListener是否要響應狀態更改“執行”任何操作,例如導航,顯示對話框等。

如果省略cubit參數,BlocBuilder將使用BlocProvider和當前函數自動執行查找BuildContext

BlocBuilder<BlocA, BlocAState>(
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

僅當您希望提供一個范圍僅限於單個窗口小部件且無法通過父級BlocProvider和當前類訪問的bloc時,才指定該bloc BuildContext

BlocBuilder<BlocA, BlocAState>(
  cubit: blocA, // provide the local cubit instance
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

為了對何時builder調用該函數進行細粒度的控制,buildWhen可以提供一個可選的選項。buildWhen獲取先前的塊狀態和當前的塊狀態並返回一個布爾值。如果buildWhen返回true,builder將使用進行調用,state並且小部件將重新生成。如果buildWhen返回false,builder則不會調用state且不會進行重建。

BlocBuilder<BlocA, BlocAState>(
  buildWhen: (previousState, state) {
    // return true/false to determine whether or not
    // to rebuild the widget with state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

BlocProvider

BlocProvider是Flutter小部件,可通過為其子元素提供塊BlocProvider.of<T>(context)。它用作依賴項注入(DI)小部件,以便可以將一個塊的單個實例提供給子樹中的多個小部件。

在大多數情況下,BlocProvider應使用它來創建新的bloc,這些bloc將可用於其余子樹。在這種情況下,由於BlocProvider負責創建塊,它將自動處理關閉bloc。

BlocProvider(
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);

默認情況下,BlocProvider將懶惰地創建bloc,這意味着create當通過查找塊時將執行該bloc BlocProvider.of<BlocA>(context)

要覆蓋此行為並強制create立即運行,lazy可以將其設置為false

BlocProvider(
  lazy: false,
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);

在某些情況下,BlocProvider可用於向小部件樹的新部分提供現有的bloc。當需要將現有bloc用於新路線時,這將是最常用的。在這種情況下,BlocProvider由於不會創建bloc,因此不會自動關閉該bloc。

BlocProvider.value(
  value: BlocProvider.of<BlocA>(context),
  child: ScreenA(),
);

然后從ChildAScreenA中檢索BlocA

// with extensions
context.read<BlocA>();

// without extensions
BlocProvider.of<BlocA>(context)復制到剪貼板錯誤復制的

MultiBlocProvider

MultiBlocProvider是Flutter小部件,可將多個BlocProvider小部件合並為一個。 MultiBlocProvider提高了可讀性,消除了嵌套多個元素的需求BlocProviders。通過使用,MultiBlocProvider我們可以從:

BlocProvider<BlocA>(
  create: (BuildContext context) => BlocA(),
  child: BlocProvider<BlocB>(
    create: (BuildContext context) => BlocB(),
    child: BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
      child: ChildA(),
    )
  )
)

至:

MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(
      create: (BuildContext context) => BlocA(),
    ),
    BlocProvider<BlocB>(
      create: (BuildContext context) => BlocB(),
    ),
    BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
    ),
  ],
  child: ChildA(),
)

BlocListener

BlocListener是Flutter小部件,它帶有BlocWidgetListener和一個可選Bloclistener以響應bloc中的狀態變化。它應用於需要在每次狀態更改時發生一次的功能,例如導航,顯示a SnackBar,顯示aDialog等。

listener`與in和函數不同,每次狀態更改(**不**包括初始狀態)僅被調用一次。`builder``BlocBuilder``void

如果省略cubit參數,BlocListener將使用BlocProvider和當前函數自動執行查找BuildContext

BlocListener<BlocA, BlocAState>(
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: Container(),
)

僅當您希望提供無法通過BlocProvider和當前訪問的bloc時,才指定該bloc BuildContext

BlocListener<BlocA, BlocAState>(
  cubit: blocA,
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: Container()
)

為了對何時listener調用該函數進行細粒度的控制,listenWhen可以提供一個可選的選項。listenWhen獲取先前的bloc狀態和當前的bloc狀態並返回一個布爾值。如果listenWhen返回true,listener將使用調用state。如果listenWhen返回false,listener則不會調用state

BlocListener<BlocA, BlocAState>(
  listenWhen: (previousState, state) {
    // return true/false to determine whether or not
    // to call listener with state
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: Container(),
)

MultiBlocListener

MultiBlocListener是Flutter小部件,可將多個BlocListener小部件合並為一個。 MultiBlocListener提高了可讀性,消除了嵌套多個元素的需求BlocListeners。通過使用,MultiBlocListener我們可以從:

BlocListener<BlocA, BlocAState>(
  listener: (context, state) {},
  child: BlocListener<BlocB, BlocBState>(
    listener: (context, state) {},
    child: BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
      child: ChildA(),
    ),
  ),
)

至:

MultiBlocListener(
  listeners: [
    BlocListener<BlocA, BlocAState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocB, BlocBState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
    ),
  ],
  child: ChildA(),
)

BlocConsumer

BlocConsumer公開builderlistener以便對新狀態做出反應。BlocConsumer與嵌套類似BlocListenerBlocBuilder但減少了所需的樣板數量。BlocConsumer僅應在需要重建UI和執行其他對狀態更改進行響應的情況下使用cubitBlocConsumer取需要BlocWidgetBuilderBlocWidgetListener和任選的cubitBlocBuilderConditionBlocListenerCondition

如果cubit省略該參數,BlocConsumer將使用BlocProvider和當前函數自動執行查找 BuildContext

BlocConsumer<BlocA, BlocAState>(
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

可選的listenWhenbuildWhen可以實現,以更精細地控制何時listenerbuilder被調用。在listenWhenbuildWhen將在每個被調用cubit state的變化。它們各自采用先前的state和當前的,state並且必須返回a bool,以確定是否將調用builderand / orlistener函數。以前state會被初始化為statecubit的時候BlocConsumer被初始化。listenWhen並且buildWhen是可選的,如果未實現,則默認為true

BlocConsumer<BlocA, BlocAState>(
  listenWhen: (previous, current) {
    // return true/false to determine whether or not
    // to invoke listener with state
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  buildWhen: (previous, current) {
    // return true/false to determine whether or not
    // to rebuild the widget with state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

RepositoryProvider

RepositoryProvider是Flutter小部件,它通過為其子節點提供存儲庫RepositoryProvider.of<T>(context)。它用作依賴項注入(DI)小部件,以便可以將存儲庫的單個實例提供給子樹中的多個小部件。BlocProvider應該用於提供塊,而RepositoryProvider只能用於存儲庫。

RepositoryProvider(
  create: (context) => RepositoryA(),
  child: ChildA(),
);

然后ChildA我們可以通過以下方式檢索Repository實例:

// with extensions
context.read<RepositoryA>();

// without extensions
RepositoryProvider.of<RepositoryA>(context)

MultiRepositoryProvider

MultiRepositoryProvider是Flutter小部件,將多個RepositoryProvider小部件合並為一個。 MultiRepositoryProvider提高了可讀性,消除了嵌套多個元素的需求RepositoryProvider。通過使用,MultiRepositoryProvider我們可以從:

RepositoryProvider<RepositoryA>(
  create: (context) => RepositoryA(),
  child: RepositoryProvider<RepositoryB>(
    create: (context) => RepositoryB(),
    child: RepositoryProvider<RepositoryC>(
      create: (context) => RepositoryC(),
      child: ChildA(),
    )
  )
)

至:

MultiRepositoryProvider(
  providers: [
    RepositoryProvider<RepositoryA>(
      create: (context) => RepositoryA(),
    ),
    RepositoryProvider<RepositoryB>(
      create: (context) => RepositoryB(),
    ),
    RepositoryProvider<RepositoryC>(
      create: (context) => RepositoryC(),
    ),
  ],
  child: ChildA(),
)

最后

相關地址

系列文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM