flutter_bloc使用解析---骚年,你还在手搭bloc吗!

2021-12-10 18:06:32 浏览数 (1)

flutter_bloc使用将从下图的三个维度说明

flutter_blocflutter_bloc

前言

  • 首先,有很多的文章在说flutter bloc模式的应用,但是百分之八九十的文章都是在说,使用StreamController StreamBuilder搭建bloc,提升性能的会加上InheritedWidget,这些文章看了很多,真正写使用bloc作者开发的flutter_bloc却少之又少。没办法,只能去bloc的github上去找使用方式,最后去bloc官网翻文档。
  • 蛋痛,各位叼毛,就不能好好说说flutter_bloc的使用吗?非要各种抄bloc模式提出作者的那俩篇文章。现在,搞的杂家这个伸手党要自己去翻文档总结(手动滑稽)。
表情1表情1

项目效果(建议PC浏览器打开)

  • Bloc范例效果
  • Cubit范例效果

问题

初次使用flutter_bloc框架,可能会有几个疑问

  • state里面定义了太多变量,某个事件只需要更新其中一个变量,其它的变量赋相同值麻烦
  • 进入某个模块,进行初始化操作:复杂的逻辑运算,网络请求等,入口在哪定义

准备工作

说明

  • 这里说明下,文章里把BlocBuilder放在顶层,因为本身页面非常简单,也是为了更好呈现页面结构,所以才放在顶层,如果需要更加颗粒化控件更新区域,请将BlocBuilder包裹你需要更新的控件区域即可

引用

  • 我觉得学习一个模式或者框架的时候,最主要的是把主流程跑通,起码可以符合标准的堆页面,这样的话,就可以把这玩意用起来,再遇到想要的什么细节,就可以自己去翻文档,毕竟大体上已经懂了,写过了几个页面,也有些体会,再去翻文档就很快能理解了
  • 实际上Bloc给的API也不多,就几个API,相关API使用说明都写在文章最后

代码语言:txt复制
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文件目录结构新建bloc文件
  • 是不是觉得,还在手动新建这些bloc文件low爆了;就好像fish_redux,不用插件,让我手动去创建那六个文件,写那些模板代码,真的要原地爆炸。

Bloc范例

效果

  • 好了,哔哔了一堆,看下咱们要用flutter_bloc实现的效果。
bloc演示bloc演示
  • 直接开Chrome演示,大家在虚拟机上跑也一样。

初始化代码

来看下这三个生成的bloc文件:main_bloc,main_event,main_state

  • main_bloc:这里就是咱们主要写逻辑的页面了
    • mapEventToState方法只有一个参数,后面自动带了一个逗号,格式化代码就分三行了,建议删掉逗号,格式化代码。
代码语言:txt复制
class MainBloc extends Bloc<MainEvent, MainState> {
  MainBloc() : super(MainInitial());

  @override
  Stream<MainState> mapEventToState(
    MainEvent event,
  ) async* {
    // TODO: implement mapEventToState
  }
}
  • main_event:这里是执行的各类事件,有点类似fish_redux的action层
代码语言:txt复制
@immutable
abstract class MainEvent {}
  • main_state:状态数据放在这里保存,中转
代码语言:txt复制
@immutable
abstract class MainState {}

class MainInitial extends MainState {}

实现

  • 主入口
代码语言:txt复制
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对象的值
代码语言:txt复制
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触发了那些事件了;维护起来也很爽,看看这里,也很快能懂页面在干嘛了
代码语言:txt复制
@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不同状态去标定业务事件,代价太大,这边用一个变量去标定,很容易轻松代替
代码语言:txt复制
class MainState{
   int selectedIndex;
   bool isExtended;
  
   MainState({this.selectedIndex, this.isExtended});
}
  • main_view
    • 这边就是咱们的界面层了,很简单,将需要刷新的组件,用BlocBuilder包裹起来,使用BlocBuilder:提供的state去赋值就ok了,context去添加执行的事件,context用StatelessWidget中提供的或者BlocBuilder提供的都行
代码语言:txt复制
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-20210612163803311image-20210612163803311
  • 生成模板代码
blocbloc
  • 支持修改后缀
image-20210612165242515image-20210612165242515
  • Wrap Widget (alt enter):RepositoryProvider,BlocConsumer,BlocBuilder,BlocProvider,BlocListener
image-20210612164717855image-20210612164717855
  • 输入 bloc 可生成快捷代码片段
image-20210612164905296image-20210612164905296

优化实现

这边完整走一下流程,让大家能有个完整的思路

  • state:首先来看看我们对state中的优化,这边进行了俩个很重要优化,增加俩个方法:init()和clone()
    • init():这里初始化统一用init()方法去管理
    • clone():这边克隆方法,是非常重要的,一旦变量达到俩位数以上,就能深刻体会该方法是多么的重要
代码语言:txt复制
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继承,在我目前的使用中,感觉它用处不大。。。
代码语言:txt复制
@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
代码语言:txt复制
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
代码语言:txt复制
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,这样可以大大的缩短我们的开发时间!

Bloc 8.0 新写法

破坏式改变

bloc8.0 的版本,对比之前的写法简直是破坏式的改变,你如果升级到bloc 8.0及其以上的版本,之前写的bloc模式写法已经完全不兼容了,mapEventToState方法直接被移除了,一运行项目,bloc内部也会给出报错,需要你手动去注册处理器

有一说一,虽然是破坏式的改变写法,但是新写法是非常的优雅,彻底改变了以前的mapEventToState方法中的各种判断Event

这种彻底不兼容的做法,确实非常的激进,但是为了优化的点,亦有可圈可点之处

新写法

  • 插件已经支持了bloc8.0 的写法
image-20211210162539060image-20211210162539060

来看下版本生成代码

  • view
代码语言:txt复制
class TestPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => TestBloc()..add(InitEvent()),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final bloc = BlocProvider.of<TestBloc>(context);

    return Container();
  }
}
  • bloc
代码语言:txt复制
class TestBloc extends Bloc<TestEvent, TestState> {
  TestBloc() : super(TestState().init()) {
    on<InitEvent>(_init);
  }

  void _init(InitEvent event, Emitter<TestState> emit) async {
    emit(state.clone());
  }
}
  • state
代码语言:txt复制
class TestState {
  TestState init() {
    return TestState();
  }

  TestState clone() {
    return TestState();
  }
}
  • event
代码语言:txt复制
abstract class TestEvent {}

class InitEvent extends TestEvent {}

可以发现Bloc层,完全不用写判断了

计数器实例

写个计数器demo,大家来感受下

  • view
代码语言:txt复制
class BlBlocCounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => BlBlocCounterBloc()..add(InitEvent()),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final bloc = BlocProvider.of<BlBlocCounterBloc>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Bloc-Bloc范例')),
      body: Center(
        child: BlocBuilder<BlBlocCounterBloc, BlBlocCounterState>(
          builder: (context, state) {
            return Text(
              '点击了 ${bloc.state.count} 次',
              style: TextStyle(fontSize: 30.0),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => bloc.add(CounterIncrementEvent()),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • bloc:所有事件入口,在顶部一目了然
代码语言:txt复制
class BlBlocCounterBloc extends Bloc<BlBlocCounterEvent, BlBlocCounterState> {
  BlBlocCounterBloc() : super(BlBlocCounterState().init()) {
    //页面初始化时刻
    on<InitEvent>(_init);
    //计数器自增
    on<CounterIncrementEvent>(_increment);
  }

  void _init(InitEvent event, Emitter<BlBlocCounterState> emit) async {
    //处理一些初始化操作,然后刷新界面
    emit(state.clone());
  }

  ///自增
  void _increment(
    CounterIncrementEvent event,
    Emitter<BlBlocCounterState> emit,
  ) {
    state.count  ;
    emit(state.clone());
  }
}
  • state
代码语言:txt复制
class BlBlocCounterState {
  late int count;

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

  BlBlocCounterState clone() {
    return BlBlocCounterState()..count = count;
  }
}
  • event
代码语言:txt复制
abstract class BlBlocCounterEvent {}

class InitEvent extends BlBlocCounterEvent {}

class CounterIncrementEvent extends BlBlocCounterEvent {}

总结

新写法,对Bloc层改动是巨大的

可以发现,主要改变的就是对事件的处理;改动后写法对比以前的写法,优雅了N倍

  • 所有事件入口全部归纳在一起
  • 可以轻松的从归纳事件入口,跳转到相应的业务逻辑
  • 对事件的处理,不用写一堆判断了!(这是破坏式改变,优化的点)

bloc层的新写法确实不错,新项目如果用bloc,可以无脑升级bloc 8.0,使用这种新写法;老项目页面多的话,改动起来,成本确实非常的大,大家自己抉择喽

Cubit范例

  • Cubit是Bloc模式的一种简化版,去掉了event这一层,对于简单的页面,用Cubit来实现,开发体验是大大的好啊,下面介绍下该种模式的写法

创建

  • 首先创建Cubit一组文件,选择“Cubit”,新建名称填写:Counter
image-20210612170053602image-20210612170053602

新建好后,他会生成三个文件:cubit,state,view;来看下生成的代码

模板代码

  • counter_cubit
代码语言:txt复制
class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(CounterState().init());
}
  • state
代码语言:txt复制
class CounterState {
  CounterState init() {
    return CounterState();
  }

  CounterState clone() {
    return CounterState();
  }
}
  • view
代码语言:txt复制
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:这个很简单,加个计时变量
代码语言:txt复制
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层一种结合后的简写
代码语言:txt复制
class BlCubitCounterCubit extends Cubit<BlCubitCounterState> {
  BlCubitCounterCubit() : super(BlCubitCounterState().init());

  ///自增
  void increment() => emit(state.clone()..count =   state.count);
}
  • view
    • view层的代码就非常简单了,点击方法里面调用cubit层的自增方法就ok了
代码语言:txt复制
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应当只能做折中方案

效果图

  • 点我体验一下
globalBlocglobalBloc

使用

来看下怎么创建和使用全局Bloc吧!

  • 主入口配置
    • 全局的Bloc创建还是蛮简单的,这边把MultiBlocProvider在Builder里面套在child上面就行了;当然了,把MultiBlocProvider套在MaterialApp上也是可以的
    • 这样我们就获得一个全局的SpanOneCubit
代码语言:txt复制
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变量
代码语言:txt复制
class SpanOneState {
  int count;

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

  ///克隆方法,针对于刷新界面数据
  SpanOneState clone() {
    return SpanOneState()..count = count;
  }
}
  • view
    • 这个页面仅仅是展示计数变量的变化,因为在主入口使用了BlocProvider创建了SpanOneCubit,所以在这个页面不需要再次创建,直接使用BlocBuilder便可以获取其state
    • 可以发现,这个页面使用了StatefulWidget,在initState周期中,初始化了数据源;这样,每次进入页面,数据源就不会保存为上一次改动的来,都会被初始化为我们想要的值;这个页面能接受到任何页面调用其事件,这样就实现类似于广播的一种效果(
代码语言:txt复制
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里面有三个事件,初始化,跳转页面,计数自增
代码语言:txt复制
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,记录下我们点击自增的次数
代码语言:txt复制
class SpanTwoState {
  int count;

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

  ///克隆方法,针对于刷新界面数据
  SpanTwoState clone() {
    return SpanTwoState()..count = count;
  }
}
  • view
    • 这地方我们需要创建使用BlocProvider一个SpanTwoCubit,这是使用Bloc的常规流程
    • 在自增的点击事件里,我们调用本模块和SpanOneCubit中的自增方法,OK,这里我们就能同步的改变SpanOneCubit模块的数据了!
代码语言:txt复制
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
    • 平平无奇的业务代码
代码语言:txt复制
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

代码语言:txt复制
BlocBuilder<BlocA, BlocAState>(
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

仅当您希望提供一个范围仅限于单个窗口小部件且无法通过父级BlocProvider和当前类访问的bloc时,才指定该bloc BuildContext

代码语言:txt复制
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且不会进行重建。

代码语言:txt复制
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。

代码语言:txt复制
BlocProvider(
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);

默认情况下,BlocProvider将懒惰地创建bloc,这意味着create当通过查找块时将执行该bloc BlocProvider.of<BlocA>(context)

要覆盖此行为并强制create立即运行,lazy可以将其设置为false

代码语言:txt复制
BlocProvider(
  lazy: false,
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);

在某些情况下,BlocProvider可用于向小部件树的新部分提供现有的bloc。当需要将现有bloc用于新路线时,这将是最常用的。在这种情况下,BlocProvider由于不会创建bloc,因此不会自动关闭该bloc。

代码语言:txt复制
BlocProvider.value(
  value: BlocProvider.of<BlocA>(context),
  child: ScreenA(),
);

然后从ChildAScreenA中检索BlocA

代码语言:txt复制
// with extensions
context.read<BlocA>();

// without extensions
BlocProvider.of<BlocA>(context)复制到剪贴板错误复制的

MultiBlocProvider

MultiBlocProvider是Flutter小部件,可将多个BlocProvider小部件合并为一个。 MultiBlocProvider提高了可读性,消除了嵌套多个元素的需求BlocProviders。通过使用,MultiBlocProvider我们可以从:

代码语言:txt复制
BlocProvider<BlocA>(
  create: (BuildContext context) => BlocA(),
  child: BlocProvider<BlocB>(
    create: (BuildContext context) => BlocB(),
    child: BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
      child: ChildA(),
    )
  )
)

至:

代码语言:txt复制
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等。

代码语言:txt复制
listener`与in和函数不同,每次状态更改(**不**包括初始状态)仅被调用一次。`builder``BlocBuilder``void

如果省略cubit参数,BlocListener将使用BlocProvider和当前函数自动执行查找BuildContext

代码语言:txt复制
BlocListener<BlocA, BlocAState>(
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: Container(),
)

仅当您希望提供无法通过BlocProvider和当前访问的bloc时,才指定该bloc BuildContext

代码语言:txt复制
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

代码语言:txt复制
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我们可以从:

代码语言:txt复制
BlocListener<BlocA, BlocAState>(
  listener: (context, state) {},
  child: BlocListener<BlocB, BlocBState>(
    listener: (context, state) {},
    child: BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
      child: ChildA(),
    ),
  ),
)

至:

代码语言:txt复制
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

代码语言:txt复制
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

代码语言:txt复制
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只能用于存储库。

代码语言:txt复制
RepositoryProvider(
  create: (context) => RepositoryA(),
  child: ChildA(),
);

然后ChildA我们可以通过以下方式检索Repository实例:

代码语言:txt复制
// with extensions
context.read<RepositoryA>();

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

MultiRepositoryProvider

MultiRepositoryProvider是Flutter小部件,将多个RepositoryProvider小部件合并为一个。 MultiRepositoryProvider提高了可读性,消除了嵌套多个元素的需求RepositoryProvider。通过使用,MultiRepositoryProvider我们可以从:

代码语言:txt复制
RepositoryProvider<RepositoryA>(
  create: (context) => RepositoryA(),
  child: RepositoryProvider<RepositoryB>(
    create: (context) => RepositoryB(),
    child: RepositoryProvider<RepositoryC>(
      create: (context) => RepositoryC(),
      child: ChildA(),
    )
  )
)

至:

代码语言:txt复制
MultiRepositoryProvider(
  providers: [
    RepositoryProvider<RepositoryA>(
      create: (context) => RepositoryA(),
    ),
    RepositoryProvider<RepositoryB>(
      create: (context) => RepositoryB(),
    ),
    RepositoryProvider<RepositoryC>(
      create: (context) => RepositoryC(),
    ),
  ],
  child: ChildA(),
)

最后

相关地址

  • demo地址: flutter_use
  • flutter_bloc相关Api白嫖地址:flutter_bloc相关Api
  • flutter_bloc
    • GitHub:https://github.com/felangel/bloc
    • Pub:https://pub.dev/packages/flutter_bloc

系列文章

  • 告别克苏鲁代码山:Flutter 改善套娃地狱问题(仿喜马拉雅PC页面举例)
  • 让Dialog拥有更多可能:这一次,解决Flutter Dialog的各种痛点!

0 人点赞