开发环境:
Mac OS 10.14.5
VSCode 1.36.1
Flutter 1.9.1+hotfix.2
前言
这是第三篇关于状态管理的文章。第一篇见
Flutter示例系列(二)之状态管理-壹(scoped_model)
,第二篇见
Flutter示例系列(二)之状态管理-贰(fish-redux)
。
Bloc使用到Stream,在
Dart基础语法之异步支持
(见公众号:Flutter小同学) 中,简单描述了Stream的用法,在
Dart异步编程:Streams
(见公众号:Flutter小同学)中,对Stream进行了简要介绍。
概念
Bloc由以下三个包组成:
-
bloc
:bloc核心库 -
flutter bloc
:Flutter组件配合bloc实现快速可交互的移动应用 -
angular bloc
:Angular组件配合bloc实现快速可交互的web应用
示例主要讲述移动应用,因此在pubspec.yaml 文件引入依赖:
dependencies:
bloc:^0.15.0
flutter_bloc:^0.21.0
状态管理的方法有很多,选择合适的至关重要。
scoped_model
使用观察者模式,将数据保存到model中,但是需要从父级往下传递,当数据发生改变时,再通知所有的监听类,更新状态。
fish_redux
基于redux设计,很好的将数据与视图分离,但是概念很多,用起来比较麻烦。
bloc
基于Stream设计,业务逻辑与视图更容易分离。设计之初就有三个核心思想:
- 简单,容易理解易上手
- 强大,由小组件构成复杂的应用
- 易测试
bloc理念
- events (事件)
events 是bloc的输入。通常根据用户交互来派发,如点击按钮、页面加载。
- states (状态)
states 是bloc的输出,也是整个应用状态的一部分。UI组件会被通知状态改变,并重绘部分视图。
- transitions(转变)
一个状态到另一个状态的改变过程叫
transitions
。由当前状态、事件和下个状态组成。
- streams(流)
stream就是一系列异步的数据。
bloc 建立在 RxDart 之上,它抽象了RxDart所有的实现细节。
要用好 bloc,对 streams 深刻理解显得尤为重要。犹如水从管道中喷涌而出,stream是管道,数据是水。
用 async* 标记函数,使用 yield 并返回包含数据的 stream。
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
yield i;
}
}
使用上面生成的stream,返回stream包含整型数值的和。用 async 标记函数,使用 await 并返回包含整型值的 Future。
Future<int> sumStream(Stream<int> stream) async {
int sum = 0;
await for (int value in stream) {
sum += value;
}
return sum;
}
主函数中调用:
void main() async {
/// Initialize a stream of integers 0-9
Stream<int> stream = countStream(10);
/// Compute the sum of the stream of integers
int sum = await sumStream(stream);
/// Print the sum
print(sum); // 45
}
- blocs
bloc(Business Logic Component)是一个组件,将包含输入事件的stream 转变成 输出状态的stream。
每个bloc必须继承 Bloc类(bloc核心包的部分):
import 'package:bloc/bloc.dart';
class CounterBloc extends Bloc<CounterEvent, int> {
}
每个bloc必须定义初始化状态,作为事件来之前的状态。如:
@overrideint get initialState => 0;
每个bloc必须实现 mapEventToState 函数,将 event 作为参数,返回带有新 states 的stream。在任何时候都可以用 currentState 访问当前状态:
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.decrement:
yield currentState - 1;
break;
case CounterEvent.increment:
yield currentState + 1;
break;
}
}
注意:bloc会忽视重复的状态。当 currentState == state时,没有 transiton 发生并且没有改变写进Stream。
每个bloc都有 dispatch 方法。Dispatch 携带 event 触发 mapEventToState,可被展示层或者bloc内部调用,进而通知bloc有新事件。
现在,创建简单的示例:
void main() {
CounterBloc bloc = CounterBloc();
for (int i = 0; i < 3; i++) {
bloc.dispatch(CounterEvent.increment);
}
}
从上面可以得出,transition 应该如下:
{
"currentState": 0,
"event": "CounterEvent.increment",
"nextState": 1
}
{
"currentState": 1,
"event": "CounterEvent.increment",
"nextState": 2
}
{
"currentState": 2,
"event": "CounterEvent.increment",
"nextState": 3
}
很可惜,除非我们重写 onTransition,否则不会看到这些转变。
onTransition
是一个可以被重写的方法,来处理每个bloc的转变。它仅在bloc的状态更新时调用。所以我们可以增加日志或者分析代码。
onError
是一个可以被重写的方法,来处理每个bloc的Exception(异常)。默认情况下,所有的异常都会被忽略且不会造成影响。
- blocDelegate
使用bloc的好处就是在一个地方可以访问所有的 Transitions。
如果想在所有 Transitions的响应中做一些事情,可以创建自己的 BlocDelegate。
class SimpleBlocDelegate extends BlocDelegate {
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print(transition);
}
}
告诉bloc使用 SimpleBlocDelegate:
void main() {
BlocSupervisor.delegate = SimpleBlocDelegate();
CounterBloc bloc = CounterBloc();
for (int i = 0; i < 3; i++) {
bloc.dispatch(CounterEvent.increment);
}
}
BlocSupervisor是一个单例,监听所有的blocs,并且委派任务到BlocDelegate。
也可以重写 onEvent,或者 onError:
class SimpleBlocDelegate extends BlocDelegate {
@override
void onEvent(Bloc bloc, Object event) {
super.onEvent(bloc, event);
print(event);
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print(transition);
}
@override
void onError(Bloc bloc, Object error, StackTrace stacktrace) {
super.onError(bloc, error, stacktrace);
print('$error, $stacktrace');
}
}
Flutter bloc 理念
1.Bloc 组件
- BlocBuilder
是一个需要
Bloc
和
builder函数
的组件。BlocBuilder 响应新状态去构建组件,类似于StreamBuilder但更简单。builder函数 可能会被调用多次,根据状态返回一个组件的纯函数。
如果想根据状态改变做一些事情(如:导航、提示语等),请看BlocListener。
如果bloc的参数省略,BlocBuilder 会用 BlocProvider 和 当前的BuildContext自动查找。
BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
// return widget here based on BlocA's state
}
)
指定bloc时,仅希望在单个组件内访问,并且无法通过 父BlocProvider 和 当前的 BuildContext 访问。
BlocBuilder<BlocA, BlocAState>(
bloc: blocA, // provide the local bloc instance
builder: (context, state) {
// return widget here based on BlocA's state
}
)
为 BlocBuilder 提供可选的 condition,来控制 builder函数何时被调用。condition 携带 bloc先前的状态和当前的状态,并返回布尔值。如果返回 true, builder 随着 currentState 被调用,组件重新构建;返回false,不会被调用也不会重建。
BlocBuilder<BlocA, BlocAState>(
condition: (previousState, currentState) {
// return true/false to determine whether or not
// to rebuild the widget with currentState
},
builder: (context, state) {
// return widget here based on BlocA's state
}
)
- BlocProvider
是通过 BlocProvider.of(context) 为子级提供bloc的 一个组件。它用作依赖项注入(DI)组件,以便bloc的单例可以提供给子树的多个组件。
大多数情况下,BlocProvider 应该构建新的 blocs 用于其他子树。BlocProvider 自己创建bloc,也自动处理bloc。
BlocProvider(
builder: (BuildContext context) => BlocA(),
child: ChildA(),
);
一些情况下,BlocProvider为组件树的新部分提供已存在的bloc。最常见的,将已存在的bloc用于新路由。此时BlocProvider不会自动处理bloc。
BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: ScreenA(),
);
然后,从ChildA() 和 ScreenA() 中检索 BlocA:
BlocProvider.of<BlocA>(context)
- MultiBlocProvider
是一个合并多个 BlocProvider组件成一个的组件。提高代码可读性并消除嵌套。
使用BlocProvider:
BlocProvider<BlocA>(
builder: (BuildContext context) => BlocA(),
child: BlocProvider<BlocB>(
builder: (BuildContext context) => BlocB(),
child: BlocProvider<BlocC>(
builder: (BuildContext context) => BlocC(),
child: ChildA(),
)
)
)
使用MultiBlocProvider:
MultiBlocProvider(
providers: [
BlocProvider<BlocA>(
builder: (BuildContext context) => BlocA(),
),
BlocProvider<BlocB>(
builder: (BuildContext context) => BlocB(),
),
BlocProvider<BlocC>(
builder: (BuildContext context) => BlocC(),
),
],
child: ChildA(),
)
- BlocListener
是一个携带BlocWidgetListener、可选bloc,并调用 listener 以响应bloc中状态改变的组件。一旦状态改变(如 导航、展示SnackBar、展示对话框等)它就需要发生。
不像 BlocBuilder 中的builder,每次状态改变(不包括初始状态 initialState)listener仅会被调用一次,并且是一个 void 函数。
如果bloc的参数被忽略,BlocListener 会利用 BlocProvider和 当前的 BuildContext自动执行查找。
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: Container(),
)
仅希望提供一个无法通过 BlocProvider和 当前的 BuildContext 访问的bloc,才指定该bloc。
BlocListener<BlocA, BlocAState>(
bloc: blocA,
listener: (context, state) {
// do stuff here based on BlocA's state
}
)
如果想掌控何时listener函数 被调用,可以为 BlocListener提供一个可选的 condition。该condition带有先前的bloc状态和当前的bloc状态,并返回布尔值。如果condition返回 true,则使用 currentState 调用listener,如果返回 false,则不调用。
BlocListener<BlocA, BlocAState>(
condition: (previousState, currentState) {
// return true/false to determine whether or not
// to call listener with currentState
},
listener: (context, state) {
// do stuff here based on BlocA's state
}
child: Container(),
)
- MultiBlocListener
是一个合并多个 BlocListener 成一个的组件。提高代码可读性并消除嵌套。
使用BlocListener:
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
child: BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
child: BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
child: ChildA(),
),
),
)
使用MultiBlocListener:
MultiBlocListener(
listeners: [
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
),
BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
),
BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
),
],
child: ChildA(),
)
- RepositoryProvider
是一个使用 RepositoryProvider.of(context) 为它的子级提供存储库的组件。被用作依赖注入(DI)组件,为多个子级组件提供 repository单例。BlocProvider 应被用作提供 blocs,但是 RepositiryProvider仅被 repositories 使用。
RepositoryProvider(
builder: (context) => RepositoryA(),
child: ChildA(),
);
然后从 ChildA 可以检索 Repository 实例:
RepositoryProvider.of<RepositoryA>(context)
- MultiRepositoryProvider
是一个合并多个 RepositoryProvider 成一个的组件。提高代码可读性并消除嵌套。
使用RepositoryProvider:
RepositoryProvider<RepositoryA>(
builder: (context) => RepositoryA(),
child: RepositoryProvider<RepositoryB>(
builder: (context) => RepositoryB(),
child: RepositoryProvider<RepositoryC>(
builder: (context) => RepositoryC(),
child: ChildA(),
)
)
)
使用MultiRepositoryProvider:
MultiRepositoryProvider(
providers: [
RepositoryProvider<RepositoryA>(
builder: (context) => RepositoryA(),
),
RepositoryProvider<RepositoryB>(
builder: (context) => RepositoryB(),
),
RepositoryProvider<RepositoryC>(
builder: (context) => RepositoryC(),
),
],
child: ChildA(),
)
使用
接下里使用 BlocBuilder 将 CounterPage 连接到 CounterBloc。
conuter_bloc.dart
enum CounterEvent { increment, decrement }
class CounterBloc extends Bloc<CounterEvent, int> {
@override
int get initialState => 0;
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.decrement:
yield currentState - 1;
break;
case CounterEvent.increment:
yield currentState + 1;
break;
}
}
}
counter_page.dart
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Center(
child: Text(
'$count',
style: TextStyle(fontSize: 24.0),
),
);
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
counterBloc.dispatch(CounterEvent.increment);
},
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () {
counterBloc.dispatch(CounterEvent.decrement);
},
),
),
],
),
);
}
}
成功的将展示层和逻辑层分离。当点击按钮时,CounterPage组件并不知道发生什么。该组件仅简单的告诉 CounterBloc 用户按下了递增或者递减按钮。
Demo
接下来,构建一个
计数器Demo
,详细注释请参考demo。
创建项目
详情见
Flutter示例系列(一)之创建项目
在 pubspec.yaml 文件中引入 flutter_bloc 依赖库:
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^0.21.0
demo只有两个按钮来控制加或减,一个文本组件显示当前值,以此设计 CounterEvents。
Counter Events
enum CounterEvent { increment, decrement }
Counter States
状态就是整型值,因此不需要自定义类
Counter Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
@override
int get initialState => 0;
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.decrement:
yield currentState - 1;
break;
case CounterEvent.increment:
yield currentState + 1;
break;
}
}
}
Counter App
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: BlocProvider<CounterBloc>(
builder: (context) => CounterBloc(),
child: CounterPage(),
),
);
}
}
使用 BlocProvider组件,让整个子树(CounterPage)都可以获取 CounterBloc实例。它也可以自动处理CounterBloc,因此不需要使用 StatefulWidget.
Counter Page
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Center(
child: Text(
'$count',
style: TextStyle(fontSize: 24.0),
),
);
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
counterBloc.dispatch(CounterEvent.increment);
},
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () {
counterBloc.dispatch(CounterEvent.decrement);
},
),
),
],
),
);
}
}
在main.dart中,把CounterPage包裹在BlocProvider中,因此使用 BlocProvider.of(context) 获取 CounterBloc实例。
为了相应状态改变时重建UI,因此使用 BlocBuilder组件。
BlocBuild带有可选的bloc参数,但是也可以指定bloc类型和状态类型,BlocBuild会自动找到bloc的类型,因此不需要显式使用 BlocProvider.of(context)。
注意:如果在BlocBuild中指定一个bloc,那作用域仅限该组件,并且无法通过父类的 BlocProvider 和当前的BuildContext 访问。
总结
由此,我们完成了计数器demo,成功的将展示层和逻辑层分离。当用户点击按钮时,CounterPage仅仅发送事件到 CounterBloc。此外,CounterBloc也仅仅转换事件返回整型值。
=================================================================
个人博客
Github
个人公众号:Flutter小同学
个人网站