在正式介绍 BLoC之前, 为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。

如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可以了。你可能并不需要状态管理,就像下面这样。

但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。

我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
Flutter 实际上在一开始就为我们提供了一种状态管理方式,那就是 StatefulWidget。但是我们很快发现,它正是造成上述原因的罪魁祸首。
在 State 属于某一个特定的 Widget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,我们增加非常多可怕的垃圾代码。
这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。

BLoC 是什么

旨在使用Widget更加加单,更加快捷,方便不同开发者都能使用,可以记录组件的各种状态,方便测试,让许多开发者遵循相同的模式和规则在一个代码库中无缝工作。

如何使用
简单例子
老规矩,我们写一个增加和减小的数字的例子,首先定义一个存储数据的Model,我们继承Equtable来方便与操作符=的判断,Equtable实现了使用props是否相等来判断两个对象是否相等,当然我们也可以自己重写操作符==来实现判断两个对象是否相等。

自己实现操作符如下:

@overridebool operator ==(Object other) {if (other is Model)return this.count == other.count &&age == other.count &&name == other.name;return false;}

使用Equtable操作符==关键代码如下:

// ignore: must_be_immutable
class Model extends Equatable {int count;int age;String name;List<String> list;Model({this.count = 0, this.name, this.list, this.age = 0});@overrideList<Object> get props => [count, name, list, age];Model addCount(int value) {return clone()..count = count + value;}Model addAge(int value) {return clone()..age = age + value;}Model clone() {return Model(count: count, name: name, list: list, age: age);}
}

构造一个装载Model数据的Cubit:

class CounterCubit extends Cubit<Model> {CounterCubit() : super(Model(count: 0, name: '老王'));void increment() {print('CounterCubit +1');emit(state.addCount(1));}void decrement() {print('CounterCubit -1');emit(state.clone());}void addAge(int v) {emit(state.addAge(v));}void addCount(int v) {emit(state.addCount(v));}
}

数据准备好之后准备展示了,首先在需要展示数据小部件上层包裹一层BlocProvider,关键代码:

BlocProvider(create: (_) => CounterCubit(),child: BaseBLoCRoute(),)

要是多个model的话和Provider写法基本一致。

MultiBlocProvider(providers: [BlocProvider(create: (_) => CounterCubit(),),BlocProvider(create: (_) => CounterCubit2(),),],child: BaseBLoCRoute(),)

然后在展示数字的widget上开始展示数据了,BlocBuilder<CounterCubit, Model>中CounterCubit是载体,Model是数据,使用builder回调来刷新UI,刷新UI的条件是buildWhen: (m1, m2) => m1.count != m2.count,当条件满足时进行回调builder.

BlocBuilder<CounterCubit, Model>(builder: (_, count) {print('CounterCubit1 ');return Row(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[Padding(child: Text('count: ${count.count}',),padding: EdgeInsets.all(20),),OutlineButton(child: Icon(Icons.arrow_drop_up),onPressed: () {context.bloc<CounterCubit>().addCount(1);},),OutlineButton(child: Icon(Icons.arrow_drop_down),onPressed: () {context.bloc<CounterCubit>().addCount(-1);},)],);},buildWhen: (m1, m2) => m1.count != m2.count,)
监听状态变更/// 监听状态变更void initState() {Bloc.observer = SimpleBlocObserver();super.initState();}/// 观察者来观察 事件的变化 可以使用默认的 [BlocObserver]
class SimpleBlocObserver extends BlocObserver {@overridevoid onEvent(Bloc bloc, Object event) {print(event);super.onEvent(bloc, event);}@overridevoid onChange(Cubit cubit, Change change) {print(change);super.onChange(cubit, change);}@overridevoid onTransition(Bloc bloc, Transition transition) {print(transition);super.onTransition(bloc, transition);}@overridevoid onError(Cubit cubit, Object error, StackTrace stackTrace) {print(error);super.onError(cubit, error, stackTrace);}
}


局部刷新

布局刷新是使用BlocBuilder来实现的,BlocBuilder<CounterCubit, Model>中CounterCubit是载体,Model是数据,使用builder回调来刷新UI,刷新UI的条件是buildWhen: (m1, m2) => m1.count != m2.count,当条件满足时进行回调builder.
本例子是多个model,多个局部UI刷新

Widget _body() {return Center(child: CustomScrollView(slivers: <Widget>[SliverToBoxAdapter(child: BlocBuilder<CounterCubit, Model>(builder: (_, count) {print('CounterCubit1 ');return Row(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[Padding(child: Text('count: ${count.count}',),padding: EdgeInsets.all(20),),OutlineButton(child: Icon(Icons.arrow_drop_up),onPressed: () {context.bloc<CounterCubit>().addCount(1);},),OutlineButton(child: Icon(Icons.arrow_drop_down),onPressed: () {context.bloc<CounterCubit>().addCount(-1);},)],);},buildWhen: (m1, m2) => m1.count != m2.count,),),SliverToBoxAdapter(child: SizedBox(height: 50,),),SliverToBoxAdapter(child: BlocBuilder<CounterCubit, Model>(builder: (_, count) {print('CounterCubit age build ');return Row(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[Padding(child: Text('age:${count.age}',),padding: EdgeInsets.all(20),),OutlineButton(child: Icon(Icons.arrow_drop_up),onPressed: () {context.bloc<CounterCubit>().addAge(1);},),OutlineButton(child: Icon(Icons.arrow_drop_down),onPressed: () {context.bloc<CounterCubit>().addAge(-1);},)],);},buildWhen: (m1, m2) => m1.age != m2.age,),),SliverToBoxAdapter(child: BlocBuilder<CounterCubit2, Model>(builder: (_, count) {print('CounterCubit2 ');return Column(children: <Widget>[Text('CounterCubit2: ${count.age}'),OutlineButton(child: Icon(Icons.add),onPressed: () {context.bloc<CounterCubit2>().addAge(1);},)],);},),)],),);}


当我们点击加好或者减号已经被SimpleBlocObserver监听到,看下打印信息,每次model变更都会通知监听者。

复杂状态变更,监听和刷新UI

一个加减例子,每次加减我们在当前组件中监听,当状态变更的时候如何实现刷新UI,而且当age+count == 10的话返回上一页。

要满足此功能的话,同一个部件至少要listener和builder,正好官方提供的BlocConsumer可以实现,如果只需要监听则需要使用BlocListener,简单来说是BlocConsumer=BlocListener+BlocBuilder.

看关键代码:

BlocConsumer<CounterCubit, Model>(builder: (ctx, state) {return Column(children: <Widget>[Text('age:${context.bloc<CounterCubit>().state.age} count:${context.bloc<CounterCubit>().state.count}'),OutlineButton(child: Text('age+1'),onPressed: () {context.bloc<CounterCubit>().addAge(1);},),OutlineButton(child: Text('age-1'),onPressed: () {context.bloc<CounterCubit>().addAge(-1);},),OutlineButton(child: Text('count+1'),onPressed: () {context.bloc<CounterCubit>().addCount(1);},),OutlineButton(child: Text('count-1'),onPressed: () {context.bloc<CounterCubit>().addCount(-1);},)],);
}, listener: (ctx, state) {if (state.age + state.count == 10)                 Navigator.maybePop(context);
})

效果如下:

复杂情况(Cubit)

登陆功能(继承 Cubit)
我们再编写一个完整登陆功能,分别用到BlocListener用来监听是否可以提交数据,用到BlocBuilder用来刷新UI,名字输入框和密码输入框分别用BlocBuilder包裹,实现局部刷新,提交按钮用BlocBuilder包裹用来展示可用和不可用状态。

此为bloc_login的官方例子的简单版本,想要了解更多请查看官方版本

观察者
观察者其实一个APP只需要写一次即可,一般在APP初始化配置即可。
我们这里只提供打印状态变更信息。

class DefaultBlocObserver extends BlocObserver {@overridevoid onChange(Cubit cubit, Change change) {if (kDebugMode)print('${cubit.toString()} new:${change.toString()} old:${cubit.state.toString()}');super.onChange(cubit, change);}
}

在初始化指定观察者

@override
void initState() {Bloc.observer=DefaultBlocObserver();super.initState();
}

或者使用默认观察者

Bloc.observer = BlocObserver();

State(Model)
存储数据的state(Model),这里我们需要账户信息,密码信息,是否可以点击登录按钮,是否正在登录这些信息。

enum LoginState {success,faild,isLoading,
}
enum BtnState { available, unAvailable }class LoginModel extends Equatable {final String name;final String password;final LoginState state;LoginModel({this.name, this.password, this.state});@overrideList<Object> get props => [name, password, state, btnVisiable];LoginModel copyWith({String name, String pwd, LoginState loginState}) {return LoginModel(name: name ?? this.name,password: pwd ?? this.password,state: loginState ?? this.state);}bool get btnVisiable =>(password?.isNotEmpty ?? false) && (name?.isNotEmpty ?? false);@overrideString toString() {return '$props';}
}

Cubit
装载state的类,当state变更需要调用emit(state),state的变更条件是==,所以我们上边的state(Model)继承了Equatable,Equatable内部实现了操作符=函数,我们只需要将它所需props重写即可。

class LoginCubit extends Cubit<LoginModel> {LoginCubit(state) : super(state);void login() async {emit(state.copyWith(loginState: LoginState.isLoading));await Future.delayed(Duration(seconds: 2));if (state.btnVisiable == true)emit(state.copyWith(loginState: LoginState.success));emit(state.copyWith(loginState: LoginState.faild));}void logOut() async {emit(state.copyWith(name: null,pwd: null,));}void changeName({String name}) {emit(state.copyWith(name: name, pwd: state.password, loginState: state.state));}void changePassword({String pwd}) {emit(state.copyWith(name: state.name, pwd: pwd, loginState: state.state));}
}

构造view
关键还是得看如何构造UI,首先输入框分别使用BlocBuilder包裹实现局部刷新,局部刷新的关键还是buildWhen得写的漂亮,密码输入框的话只需要判断密码是否改变即可,账号的话只需要判断账号是否发生改变即可,
按钮也是如此,在UI外层使用listener来监听状态变更,取所需要的状态跳转新的页面或者弹窗。

首先看下输入框关键代码:

class TextFiledNameRoute extends StatelessWidget {@overrideWidget build(BuildContext context) {return BlocBuilder<LoginCubit, LoginModel>(builder: (BuildContext context, LoginModel state) {return TextField(onChanged: (v) {context.bloc<LoginCubit>().changeName(name: v);},decoration: InputDecoration(labelText: 'name',errorText: state.name?.isEmpty ?? false ? 'name不可用' : null),);},buildWhen: (previos, current) => previos.name != current.name);}
}class TextFiledPasswordRoute extends StatelessWidget {@overrideWidget build(BuildContext context) {return BlocBuilder<LoginCubit, LoginModel>(builder: (BuildContext context, LoginModel state) {return TextField(onChanged: (v) {context.bloc<LoginCubit>().changePassword(pwd: v);},decoration: InputDecoration(labelText: 'password',errorText:state.password?.isEmpty ?? false ? 'password不可用' : null),);},buildWhen: (previos, current) => previos.password != current.password);}
}

这里我们实现了登陆成功弹出snackBar.

看下效果图哦:


复杂情况(Bloc)
情况1都我们手动emit(state),那么有没有使用流技术来直接监听的呢?答案是有,那么我们再实现一遍使用bloc的登陆功能。

state(数据载体)
首先我们使用 一个抽象类来定义事件,然后各种小的事件都继承它,比如:NameEvent装载了姓名信息,PasswordEvent装载了密码信息,SubmittedEvent装载了提交信息,简单来讲,event就是每一个按钮点击事件或者valueChange事件触发的动作,最好下载代码之后自己对比下,然后自己从简单例子写,此为稍微复杂情况,看下关键代码:

/// 登陆相关的事件
abstract class LoginEvent extends Equatable {const LoginEvent();@overrideList<Object> get props => [];
}/// 修改密码
class LoginChagnePassword extends LoginEvent {final String password;const LoginChagnePassword({this.password});@overrideList<Object> get props => [password];
}/// 修改账户
class LoginChagneName extends LoginEvent {final String name;const LoginChagneName({this.name});@overrideList<Object> get props => [name];
}/// 提交事件
class LoginSubmitted extends LoginEvent {const LoginSubmitted();@overrideList<Object> get props => [];
}

存储数据的state,在LoginBloc中将event转换成state,那么state需要存储什么数据呢?需要存储账户信息、密码、登陆状态等信息。

/// 事件变更状态[正在请求,报错,登陆成功,初始化]
enum Login2Progress { isRequesting, error, success, init }/// 存储数据的model 在[bloc]中称作[state]
class LoginState2 extends Equatable {final String name;final String password;final Login2Progress progress;LoginState2({this.name, this.password, this.progress = Login2Progress.init});@overrideList<Object> get props => [name, password, btnVisiable, progress];LoginState2 copyWith({String name, String pwd, Login2Progress login2progress}) {return LoginState2(name: name ?? this.name,password: pwd ?? this.password,progress: login2progress ?? this.progress);}/// 使用 [UserName] &&[UserPassword]来校验规则bool get btnVisiable => nameVisiable && passwordVisiable;bool get nameVisiable => UserName(name).visiable;bool get passwordVisiable => UserPassword(password).visiable;/// 是否展示名字错误信息bool get showNameErrorText {if (name?.isEmpty ?? true) return false;return nameVisiable == false;}/// 是否展示密码错误信息bool get showPasswordErrorText {if (password?.isEmpty ?? true) return false;return passwordVisiable == false;}@overrideString toString() {return '$props';}
}

event和state写好了,怎么将event转换成state呢?首先新建一个类继承Bloc,覆盖函数mapEventToState,利用这个函数参数event来对state,进行转换,中间因为用到了虚拟的网络登陆,耗时操作和状态变更,所以使用了yield*返回了另外一个流函数。

class LoginBloc extends Bloc<LoginEvent, LoginState2> {LoginBloc(initialState) : super(initialState);@overrideStream<LoginState2> mapEventToState(event) async* {if (event is LoginChagneName) {yield _mapChangeUserNameToState(event, state);} else if (event is LoginChagnePassword) {yield _mapChangePasswordToState(event, state);} else if (event is LoginSubmitted) {yield* _mapSubmittedToState(event, state);}}/// 改变密码LoginState2 _mapChangePasswordToState(LoginChagnePassword event, LoginState2 state2) {return state2.copyWith(pwd: event.password ?? '');}/// 改变名字LoginState2 _mapChangeUserNameToState(LoginChagneName event, LoginState2 state2) {return state2.copyWith(name: event.name ?? '');}/// 提交Stream<LoginState2> _mapSubmittedToState(LoginSubmitted event, LoginState2 state2) async* {try {if (state2.name.isNotEmpty && state2.password.isNotEmpty) {yield state2.copyWith(login2progress: Login2Progress.isRequesting);await Future.delayed(Duration(seconds: 2));yield state2.copyWith(login2progress: Login2Progress.success);yield state2.copyWith(login2progress: Login2Progress.init);}} on Exception catch (e) {yield state2.copyWith(login2progress: Login2Progress.error);}}
}

state和event事件整理成图方便理解一下:

构造view
样式我们还是使用上边的 ,但是发送事件却不一样,原因是继承bloc其实是实现了EventSink的接口,使用add()触发监听。

class TextFiledNameRoute extends StatelessWidget {@overrideWidget build(BuildContext context) {return BlocBuilder<LoginBloc, LoginState2>(builder: (BuildContext context, LoginState2 state) {return TextField(onChanged: (v) {context.bloc<LoginBloc>().add(LoginChagneName(name: v));},textAlign: TextAlign.center,decoration: InputDecoration(labelText: 'name',errorText:(state.showNameErrorText == true) ? 'name不可用' : null),);},buildWhen: (previos, current) => previos.name != current.name);}
}

完整的效果是:

BLoC 流程
首先view部件持有Cubit,Cubit持有状态(Model),当状态(Model)发生变更时通知Cubit,Cubit依次通知listener、BlocBulder.builder进行刷新UI,每次状态变更都会通知BlocObserver,可以做到全局的状态监听。

千言万语不如一张图:



更多关于人工智能的文章,敬请访问:FlyAI-AI竞赛服务平台学习圈学习;同时FlyAI欢迎广大算法工程师在平台发文,获得更多原创奖励。此外,FlyAI竞赛平台提供大量数据型赛题供学习党和竞赛党参与,免费GPU试用,更多大赛经验分享。

FlyAI小课堂:Flutter 状态管理之BLoC相关推荐

  1. Flutter 状态管理之Bloc上

    前言:Flutter 的状态管理插件有很多,比如 Provider,GetX 还有本篇要讲述的 Bloc .Bloc 目前最新的版本是 flutter_bloc: ^8.0.1. BLoC 依赖 St ...

  2. Flutter 状态管理之Bloc下

    这篇是使用 Bloc 来实现业务逻辑与UI分离.主要就是慕课网课程列表的网络请求并且展示. 首先定义一个基础事件的类,如下: abstract class LessonEvent {} 然后我定义了3 ...

  3. 小程序全局状态管理,在页面中获取globalData和使用globalSetData

    GitHub: https://github.com/WozHuang/mp-extend 主要目标 微信小程序官方没有提供类似vuex.redux全局状态管理的解决方案,但是在一个完整的项目中各组件 ...

  4. 微信小程序----全局状态管理 (便于全局埋点等操作)

    说明 由于我是一个有着一颗玻璃心的博主,导致在2018年后博客很少更新.原因是由于我的分享并没有解决到部分人的问题,而导致被骂了.当时这颗玻璃心就碎了,所以这两年以来很是消极,博客很少更新.这里给那些 ...

  5. Flutter 状态管理指南之 Provider

    2019 Google I/O 大会,Flutter 团队在"Pragmatic State Management in Flutter "演讲上正式介绍了 Provider.自此 ...

  6. flutter 状态管理 flutter_bloc 的使用以及总结

    Bloc介绍 flutter_bloc 8.0,直接把mapEventToState方法去掉,需要手动注册事件处理器,不用再写if else 来判断event ,也不用写yield flutter_b ...

  7. Flutter状态管理1-ChangeNotifierProvider的使用

    关于Flutter中的状态管理,可以参考官网的介绍:Simple app state management 中文网的介绍:简单的应用状态管理 Flutter 官方的两个sample: provider ...

  8. Flutter 状态管理

    目录 一.状态管理简介 1.1 为什么需要状态管理 1.1.1 Flutter与adr/ios UI框架区别 1.1.2 状态管理框架使用场景 1.2 需要解决的问题 二.状态管理框架现有方案调研 2 ...

  9. 微信小程序全局状态管理store

    wxMiniStore 一个基于原生小程序的Mini全局状态管理库,跨页面/组件的数据共享. 1.安装 npm init npm install wxministore -S 2.微信开发者工具中勾选 ...

最新文章

  1. 2018年4月22日笔记
  2. Java8 PriorityBlockingQueue源码分析
  3. linux在双系统中消失了,双系统重新安装windows后,ubuntu选项消失
  4. 对Linux课程内容的建议,Linux课程笔记 Day01 课程内容总结(示例代码)
  5. keystone 手动建立租户,用户,角色,服务,端口
  6. Tomcat根目录下work文件夹的作用
  7. 经历过贫穷,才知道做穷人意味着什么
  8. SVN server
  9. Boundary Representations
  10. Remap 后的 USART1 不能发送数据
  11. wordpress七步曲
  12. 我的工程学导论学习心得1
  13. python列表元素可以重复吗_Python列表中的元素重复
  14. 解决win10微软应用商店打不开的问题
  15. 应用程序开发选择工具应注重运行效率还是易用性
  16. 【程序员笑话】让你泪流满面的瞬间
  17. divi 相关主题推荐
  18. 龙之谷服务器仓库在哪个位置,全区全服版本更新至Ver.190
  19. 租房子,光看数据怎么够,Python爬取某站租房图片
  20. csgo调出参数_CSGO全参数设置

热门文章

  1. 提高blog访问量的秘技与非秘技
  2. 随机变量及其分布函数
  3. CocosCreator接入穿山甲广告3-底部banner
  4. 新概念英语第一册——5-8笔记
  5. HTML给div设置百分比高度无效的解决方式 - 库塔姆斯 - CSDN博客
  6. 慕课HTML学习之网页链接
  7. 【老生谈算法】matlab实现图像平滑算法——图像平滑算法
  8. mysql 5.6 多实例_MySQL 5.6 多实例安装
  9. VS Code 设置代码自动保存
  10. 电子信息、通信、电类专业将会遇到的面试题大全!精!!!看了让人大吃一惊......