在正式介绍 BLoC之前, 为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。
如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可以了。你可能并不需要状态管理,就像下面这样。
Flutter 状态管理之BLoC - 图1
但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。
Flutter 状态管理之BLoC - 图2
我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
Flutter 实际上在一开始就为我们提供了一种状态管理方式,那就是 StatefulWidget。但是我们很快发现,它正是造成上述原因的罪魁祸首。
State 属于某一个特定的 Widget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,我们增加非常多可怕的垃圾代码。
这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。

BLoC 是什么

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

如何使用

简单例子

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

自己实现操作符如下:

  1. @override
  2. bool operator ==(Object other) {
  3. if (other is Model)
  4. return this.count == other.count &&
  5. age == other.count &&
  6. name == other.name;
  7. return false;
  8. }

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

  1. // ignore: must_be_immutable
  2. class Model extends Equatable {
  3. int count;
  4. int age;
  5. String name;
  6. List<String> list;
  7. Model({this.count = 0, this.name, this.list, this.age = 0});
  8. @override
  9. List<Object> get props => [count, name, list, age];
  10. Model addCount(int value) {
  11. return clone()..count = count + value;
  12. }
  13. Model addAge(int value) {
  14. return clone()..age = age + value;
  15. }
  16. Model clone() {
  17. return Model(count: count, name: name, list: list, age: age);
  18. }
  19. }

构造一个装载Model数据的Cubit

  1. class CounterCubit extends Cubit<Model> {
  2. CounterCubit() : super(Model(count: 0, name: '老王'));
  3. void increment() {
  4. print('CounterCubit +1');
  5. emit(state.addCount(1));
  6. }
  7. void decrement() {
  8. print('CounterCubit -1');
  9. emit(state.clone());
  10. }
  11. void addAge(int v) {
  12. emit(state.addAge(v));
  13. }
  14. void addCount(int v) {
  15. emit(state.addCount(v));
  16. }
  17. }

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

  1. BlocProvider(
  2. create: (_) => CounterCubit(),
  3. child: BaseBLoCRoute(),
  4. )

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

  1. MultiBlocProvider(
  2. providers: [
  3. BlocProvider(
  4. create: (_) => CounterCubit(),
  5. ),
  6. BlocProvider(
  7. create: (_) => CounterCubit2(),
  8. ),
  9. ],
  10. child: BaseBLoCRoute(),
  11. )

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

  1. BlocBuilder<CounterCubit, Model>(
  2. builder: (_, count) {
  3. print('CounterCubit1 ');
  4. return Row(
  5. mainAxisAlignment: MainAxisAlignment.center,
  6. children: <Widget>[
  7. Padding(
  8. child: Text(
  9. 'count: ${count.count}',
  10. ),
  11. padding: EdgeInsets.all(20),
  12. ),
  13. OutlineButton(
  14. child: Icon(Icons.arrow_drop_up),
  15. onPressed: () {
  16. context.bloc<CounterCubit>().addCount(1);
  17. },
  18. ),
  19. OutlineButton(
  20. child: Icon(Icons.arrow_drop_down),
  21. onPressed: () {
  22. context.bloc<CounterCubit>().addCount(-1);
  23. },
  24. )
  25. ],
  26. );
  27. },
  28. buildWhen: (m1, m2) => m1.count != m2.count,
  29. )

监听状态变更

  1. /// 监听状态变更
  2. void initState() {
  3. Bloc.observer = SimpleBlocObserver();
  4. super.initState();
  5. }
  6. /// 观察者来观察 事件的变化 可以使用默认的 [BlocObserver]
  7. class SimpleBlocObserver extends BlocObserver {
  8. @override
  9. void onEvent(Bloc bloc, Object event) {
  10. print(event);
  11. super.onEvent(bloc, event);
  12. }
  13. @override
  14. void onChange(Cubit cubit, Change change) {
  15. print(change);
  16. super.onChange(cubit, change);
  17. }
  18. @override
  19. void onTransition(Bloc bloc, Transition transition) {
  20. print(transition);
  21. super.onTransition(bloc, transition);
  22. }
  23. @override
  24. void onError(Cubit cubit, Object error, StackTrace stackTrace) {
  25. print(error);
  26. super.onError(cubit, error, stackTrace);
  27. }
  28. }

Flutter 状态管理之BLoC - 图3

局部刷新

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

  1. Widget _body() {
  2. return Center(
  3. child: CustomScrollView(
  4. slivers: <Widget>[
  5. SliverToBoxAdapter(
  6. child: BlocBuilder<CounterCubit, Model>(
  7. builder: (_, count) {
  8. print('CounterCubit1 ');
  9. return Row(
  10. mainAxisAlignment: MainAxisAlignment.center,
  11. children: <Widget>[
  12. Padding(
  13. child: Text(
  14. 'count: ${count.count}',
  15. ),
  16. padding: EdgeInsets.all(20),
  17. ),
  18. OutlineButton(
  19. child: Icon(Icons.arrow_drop_up),
  20. onPressed: () {
  21. context.bloc<CounterCubit>().addCount(1);
  22. },
  23. ),
  24. OutlineButton(
  25. child: Icon(Icons.arrow_drop_down),
  26. onPressed: () {
  27. context.bloc<CounterCubit>().addCount(-1);
  28. },
  29. )
  30. ],
  31. );
  32. },
  33. buildWhen: (m1, m2) => m1.count != m2.count,
  34. ),
  35. ),
  36. SliverToBoxAdapter(
  37. child: SizedBox(
  38. height: 50,
  39. ),
  40. ),
  41. SliverToBoxAdapter(
  42. child: BlocBuilder<CounterCubit, Model>(
  43. builder: (_, count) {
  44. print('CounterCubit age build ');
  45. return Row(
  46. mainAxisAlignment: MainAxisAlignment.center,
  47. children: <Widget>[
  48. Padding(
  49. child: Text(
  50. 'age:${count.age}',
  51. ),
  52. padding: EdgeInsets.all(20),
  53. ),
  54. OutlineButton(
  55. child: Icon(Icons.arrow_drop_up),
  56. onPressed: () {
  57. context.bloc<CounterCubit>().addAge(1);
  58. },
  59. ),
  60. OutlineButton(
  61. child: Icon(Icons.arrow_drop_down),
  62. onPressed: () {
  63. context.bloc<CounterCubit>().addAge(-1);
  64. },
  65. )
  66. ],
  67. );
  68. },
  69. buildWhen: (m1, m2) => m1.age != m2.age,
  70. ),
  71. ),
  72. SliverToBoxAdapter(
  73. child: BlocBuilder<CounterCubit2, Model>(
  74. builder: (_, count) {
  75. print('CounterCubit2 ');
  76. return Column(
  77. children: <Widget>[
  78. Text('CounterCubit2: ${count.age}'),
  79. OutlineButton(
  80. child: Icon(Icons.add),
  81. onPressed: () {
  82. context.bloc<CounterCubit2>().addAge(1);
  83. },
  84. )
  85. ],
  86. );
  87. },
  88. ),
  89. )
  90. ],
  91. ),
  92. );
  93. }

Flutter 状态管理之BLoC - 图4

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

  1. flutter: Change { currentState: Model, nextState: Model }
  2. flutter: CounterCubit2
  3. flutter: Change { currentState: Model, nextState: Model }
  4. flutter: CounterCubit2

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

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

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

看关键代码:

  1. BlocConsumer<CounterCubit, Model>(builder: (ctx, state) {
  2. return Column(
  3. children: <Widget>[
  4. Text(
  5. 'age:${context.bloc<CounterCubit>().state.age} count:${context.bloc<CounterCubit>().state.count}'),
  6. OutlineButton(
  7. child: Text('age+1'),
  8. onPressed: () {
  9. context.bloc<CounterCubit>().addAge(1);
  10. },
  11. ),
  12. OutlineButton(
  13. child: Text('age-1'),
  14. onPressed: () {
  15. context.bloc<CounterCubit>().addAge(-1);
  16. },
  17. ),
  18. OutlineButton(
  19. child: Text('count+1'),
  20. onPressed: () {
  21. context.bloc<CounterCubit>().addCount(1);
  22. },
  23. ),
  24. OutlineButton(
  25. child: Text('count-1'),
  26. onPressed: () {
  27. context.bloc<CounterCubit>().addCount(-1);
  28. },
  29. )
  30. ],
  31. );
  32. }, listener: (ctx, state) {
  33. if (state.age + state.count == 10) Navigator.maybePop(context);
  34. })

效果如下:

Flutter 状态管理之BLoC - 图5

复杂情况(Cubit)

登陆功能(继承 Cubit)

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

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

观察者

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

  1. class DefaultBlocObserver extends BlocObserver {
  2. @override
  3. void onChange(Cubit cubit, Change change) {
  4. if (kDebugMode)
  5. print(
  6. '${cubit.toString()} new:${change.toString()} old:${cubit.state.toString()}');
  7. super.onChange(cubit, change);
  8. }
  9. }

在初始化指定观察者

  1. @override
  2. void initState() {
  3. Bloc.observer=DefaultBlocObserver();
  4. super.initState();
  5. }

或者使用默认观察者

  1. Bloc.observer = BlocObserver();

State(Model)

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

  1. enum LoginState {
  2. success,
  3. faild,
  4. isLoading,
  5. }
  6. enum BtnState { available, unAvailable }
  7. class LoginModel extends Equatable {
  8. final String name;
  9. final String password;
  10. final LoginState state;
  11. LoginModel({this.name, this.password, this.state});
  12. @override
  13. List<Object> get props => [name, password, state, btnVisiable];
  14. LoginModel copyWith({String name, String pwd, LoginState loginState}) {
  15. return LoginModel(
  16. name: name ?? this.name,
  17. password: pwd ?? this.password,
  18. state: loginState ?? this.state);
  19. }
  20. bool get btnVisiable =>
  21. (password?.isNotEmpty ?? false) && (name?.isNotEmpty ?? false);
  22. @override
  23. String toString() {
  24. return '$props';
  25. }
  26. }

Cubit

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

  1. class LoginCubit extends Cubit<LoginModel> {
  2. LoginCubit(state) : super(state);
  3. void login() async {
  4. emit(state.copyWith(loginState: LoginState.isLoading));
  5. await Future.delayed(Duration(seconds: 2));
  6. if (state.btnVisiable == true)
  7. emit(state.copyWith(loginState: LoginState.success));
  8. emit(state.copyWith(loginState: LoginState.faild));
  9. }
  10. void logOut() async {
  11. emit(state.copyWith(
  12. name: null,
  13. pwd: null,
  14. ));
  15. }
  16. void changeName({String name}) {
  17. emit(state.copyWith(
  18. name: name, pwd: state.password, loginState: state.state));
  19. }
  20. void changePassword({String pwd}) {
  21. emit(state.copyWith(name: state.name, pwd: pwd, loginState: state.state));
  22. }
  23. }

构造view

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

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

  1. class TextFiledNameRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return BlocBuilder<LoginCubit, LoginModel>(
  5. builder: (BuildContext context, LoginModel state) {
  6. return TextField(
  7. onChanged: (v) {
  8. context.bloc<LoginCubit>().changeName(name: v);
  9. },
  10. decoration: InputDecoration(
  11. labelText: 'name',
  12. errorText: state.name?.isEmpty ?? false ? 'name不可用' : null),
  13. );
  14. },
  15. buildWhen: (previos, current) => previos.name != current.name);
  16. }
  17. }
  18. class TextFiledPasswordRoute extends StatelessWidget {
  19. @override
  20. Widget build(BuildContext context) {
  21. return BlocBuilder<LoginCubit, LoginModel>(
  22. builder: (BuildContext context, LoginModel state) {
  23. return TextField(
  24. onChanged: (v) {
  25. context.bloc<LoginCubit>().changePassword(pwd: v);
  26. },
  27. decoration: InputDecoration(
  28. labelText: 'password',
  29. errorText:
  30. state.password?.isEmpty ?? false ? 'password不可用' : null),
  31. );
  32. },
  33. buildWhen: (previos, current) => previos.password != current.password);
  34. }
  35. }

按钮根据不同的状态来显示可用或不可用或正在提交的动画效果。

  1. class LoginButton extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return BlocBuilder<LoginCubit, LoginModel>(
  5. builder: (BuildContext context, LoginModel state) {
  6. switch (state.state) {
  7. case LoginState.isLoading:
  8. return const CircularProgressIndicator();
  9. default:
  10. return RaisedButton(
  11. child: const Text('login'),
  12. onPressed: state.btnVisiable
  13. ? () {
  14. context.bloc<LoginCubit>().login();
  15. }
  16. : null,
  17. );
  18. }
  19. },
  20. buildWhen: (previos, current) =>
  21. previos.btnVisiable != current.btnVisiable ||
  22. (current.state != previos.state));
  23. }
  24. }

小部件写好了,那么我们将他们组合起来

  1. class BaseLoginPageRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return BlocProvider(
  5. create: (_) => LoginCubit(LoginModel()),
  6. child: BaseLoginPage(),
  7. );
  8. }
  9. static String routeName = '/BaseLoginPageRoute';
  10. MaterialPageRoute get route =>
  11. MaterialPageRoute(builder: (_) => BaseLoginPageRoute());
  12. }
  13. class BaseLoginPage extends StatefulWidget {
  14. BaseLoginPage({Key key}) : super(key: key);
  15. @override
  16. _BaseLoginPageState createState() => _BaseLoginPageState();
  17. }
  18. class _BaseLoginPageState extends State<BaseLoginPage> {
  19. @override
  20. Widget build(BuildContext context) {
  21. return Scaffold(
  22. appBar: AppBar(
  23. title: Text('loginBLoC Cubit'),
  24. ),
  25. body: _body(),
  26. );
  27. }
  28. Widget _body() {
  29. return BlocListener<LoginCubit, LoginModel>(
  30. listener: (context, state) {
  31. if (state.state == LoginState.success) {
  32. Scaffold.of(context)
  33. ..hideCurrentSnackBar()
  34. ..showSnackBar(const SnackBar(content: Text('登陆成功')));
  35. }
  36. },
  37. child: Center(
  38. child: Column(
  39. children: <Widget>[
  40. TextFiledNameRoute(),
  41. TextFiledPasswordRoute(),
  42. const SizedBox(
  43. height: 20,
  44. ),
  45. LoginButton()
  46. ],
  47. ),
  48. ),
  49. );
  50. }
  51. @override
  52. void initState() {
  53. Bloc.observer = BlocObserver();
  54. super.initState();
  55. }
  56. }

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

看下效果图哦:

Flutter 状态管理之BLoC - 图6

复杂情况(Bloc)

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

state(数据载体)

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

  1. /// 登陆相关的事件
  2. abstract class LoginEvent extends Equatable {
  3. const LoginEvent();
  4. @override
  5. List<Object> get props => [];
  6. }
  7. /// 修改密码
  8. class LoginChagnePassword extends LoginEvent {
  9. final String password;
  10. const LoginChagnePassword({this.password});
  11. @override
  12. List<Object> get props => [password];
  13. }
  14. /// 修改账户
  15. class LoginChagneName extends LoginEvent {
  16. final String name;
  17. const LoginChagneName({this.name});
  18. @override
  19. List<Object> get props => [name];
  20. }
  21. /// 提交事件
  22. class LoginSubmitted extends LoginEvent {
  23. const LoginSubmitted();
  24. @override
  25. List<Object> get props => [];
  26. }

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

  1. /// 事件变更状态[正在请求,报错,登陆成功,初始化]
  2. enum Login2Progress { isRequesting, error, success, init }
  3. /// 存储数据的model 在[bloc]中称作[state]
  4. class LoginState2 extends Equatable {
  5. final String name;
  6. final String password;
  7. final Login2Progress progress;
  8. LoginState2({this.name, this.password, this.progress = Login2Progress.init});
  9. @override
  10. List<Object> get props => [name, password, btnVisiable, progress];
  11. LoginState2 copyWith(
  12. {String name, String pwd, Login2Progress login2progress}) {
  13. return LoginState2(
  14. name: name ?? this.name,
  15. password: pwd ?? this.password,
  16. progress: login2progress ?? this.progress);
  17. }
  18. /// 使用 [UserName] &&[UserPassword]来校验规则
  19. bool get btnVisiable => nameVisiable && passwordVisiable;
  20. bool get nameVisiable => UserName(name).visiable;
  21. bool get passwordVisiable => UserPassword(password).visiable;
  22. /// 是否展示名字错误信息
  23. bool get showNameErrorText {
  24. if (name?.isEmpty ?? true) return false;
  25. return nameVisiable == false;
  26. }
  27. /// 是否展示密码错误信息
  28. bool get showPasswordErrorText {
  29. if (password?.isEmpty ?? true) return false;
  30. return passwordVisiable == false;
  31. }
  32. @override
  33. String toString() {
  34. return '$props';
  35. }
  36. }

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

  1. class LoginBloc extends Bloc<LoginEvent, LoginState2> {
  2. LoginBloc(initialState) : super(initialState);
  3. @override
  4. Stream<LoginState2> mapEventToState(event) async* {
  5. if (event is LoginChagneName) {
  6. yield _mapChangeUserNameToState(event, state);
  7. } else if (event is LoginChagnePassword) {
  8. yield _mapChangePasswordToState(event, state);
  9. } else if (event is LoginSubmitted) {
  10. yield* _mapSubmittedToState(event, state);
  11. }
  12. }
  13. /// 改变密码
  14. LoginState2 _mapChangePasswordToState(
  15. LoginChagnePassword event, LoginState2 state2) {
  16. return state2.copyWith(pwd: event.password ?? '');
  17. }
  18. /// 改变名字
  19. LoginState2 _mapChangeUserNameToState(
  20. LoginChagneName event, LoginState2 state2) {
  21. return state2.copyWith(name: event.name ?? '');
  22. }
  23. /// 提交
  24. Stream<LoginState2> _mapSubmittedToState(
  25. LoginSubmitted event, LoginState2 state2) async* {
  26. try {
  27. if (state2.name.isNotEmpty && state2.password.isNotEmpty) {
  28. yield state2.copyWith(login2progress: Login2Progress.isRequesting);
  29. await Future.delayed(Duration(seconds: 2));
  30. yield state2.copyWith(login2progress: Login2Progress.success);
  31. yield state2.copyWith(login2progress: Login2Progress.init);
  32. }
  33. } on Exception catch (e) {
  34. yield state2.copyWith(login2progress: Login2Progress.error);
  35. }
  36. }
  37. }

stateevent事件整理成图方便理解一下:

Flutter 状态管理之BLoC - 图7

构造view

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

  1. class TextFiledNameRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return BlocBuilder<LoginBloc, LoginState2>(
  5. builder: (BuildContext context, LoginState2 state) {
  6. return TextField(
  7. onChanged: (v) {
  8. context.bloc<LoginBloc>().add(LoginChagneName(name: v));
  9. },
  10. textAlign: TextAlign.center,
  11. decoration: InputDecoration(
  12. labelText: 'name',
  13. errorText:
  14. (state.showNameErrorText == true) ? 'name不可用' : null),
  15. );
  16. },
  17. buildWhen: (previos, current) => previos.name != current.name);
  18. }
  19. }

完整的效果是:

Flutter 状态管理之BLoC - 图8

BLoC 流程

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

千言万语不如一张图:

Flutter 状态管理之BLoC - 图9

参考