flutter_bloc今天发布了4.0.0版本,现关于其使用和原理做一个简单的总结。
Flutter状态管理系列:
Flutter状态管理1-ChangeNotifierProvider的使用
Flutter状态管理2-ChangeNotifierProxyProvider
Flutter状态管理3-InheritedWidget
flutter_bloc官网:
https://github.com/felangel/bloc
https://bloclibrary.dev/#/flutterbloccoreconcepts?id=flutter-bloc-core-concepts
pub.dev上的介绍,包括了多个Examples:
https://pub.dev/packages/flutter_bloc
flutter_bloc 4.0.0内部 使用了provider状态管理框架4.0.5版本,对外提供的相关API也类似,同时flutter_bloc基于了Stream做了一些包装,并在内部使用了rxDart,归根到底还是一个观察者模式的使用,通过该方式可以将原来的StatefulWidget转换成StatelessWidget,避免了多次调用setState()导致的性能损耗。
这里以Counter为例子,简单介绍其使用:
https://bloclibrary.dev/#/fluttercountertutorial
这里介绍一个库equatable,可以在使用对象比较时简化我们定义类的“==”和“hashCode”工作
https://pub.dev/packages/equatable
name: flutter_counter
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^4.0.0
meta: ^1.1.6
equatable: ^1.1.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
由于bloc(Business Logic Component)其实会将事件Event的变化映射到状态State的变化(通过自定义Bloc来完成两者的转化),来将业务逻辑Business Logic从UI层抽离出来,所以我们需要先定义事件Event。
enum CounterEvent { increment, decrement }
官网的Counter例子使用的是枚举类型定义事件类,我们也可以按照Flutter Bloc构建轻量级MVVM中的方式来定义。
abstract class CounterEvent extends Equatable {
const CounterEvent();
}
//点位增加 传入某个值,做更新操作
class ButtonPressAddX extends CounterEvent {
final int x;
const ButtonPressAddX({
@required this.x,
});
@override
// TODO: implement props
List<Object> get props => [x];
@override
String toString() {
return 'ButtonPressAddX { x: $x }';
}
}
//点位数值整体重置,所以传入整个状态对象, event 本身不对值进行篡改
class ButtonPressedAdd extends CounterEvent {
final PointState point;
const ButtonPressedAdd({
@required this.point,
});
@override
List<Object> get props => [point];
@override
String toString() => getString();
String getString() {
var x = point.x;
var y = point.y;
var z = point.z;
return 'ButtonPressedAdd { x: $x, y: $y , z: $z }';
}
}
class ButtonPressedReduce extends CounterEvent {
final PointState point;
const ButtonPressedReduce({
@required this.point,
});
@override
List<Object> get props => [point];
@override
String toString() => getString();
String getString() {
var x = point.x;
var y = point.y;
var z = point.z;
return 'ButtonPressedReduce { x: $x, y: $y , z: $z }';
}
}
官网的Counter例子里的State是一个int值,所以不需要再自定义类型,当然我们可以自定义一个类:
abstract class MyState extends Equatable {
const MyState();
}
class StateA extends MyState {
final String property;
const StateA(this.property);
@override
List<Object> get props => [property]; // pass all properties to props
}
这里以官网的Counter例子为例,在自定义的Bloc中完成事件Event和State状态的转换,这里通过复写两个方法,一个初始化状态的方法,一个mapEventToState转换方法,注意在该方法中每次都要返回一个新的State对象:
@override
Stream<MyState> mapEventToState(MyEvent event) async* {
// always create a new instance of the state you are going to yield
yield state.copyWith(property: event.property);
}
@override
Stream<MyState> mapEventToState(MyEvent event) async* {
final data = _getData(event.info);
// always create a new instance of the state you are going to yield
yield MyState(data: data);
}
或者按照Flutter Bloc构建轻量级MVVM中的方式来定义CounterBloc:
class CounterBloc extends Bloc<CounterEvent, PointState> {
var point;
CounterBloc({this.point});
//初始化 状态机 如果没传参 就构建一个(0,0,0)的对象
@override
PointState get initialState => point != null ? point : PointState(0, 0, 0);
@override
Stream<PointState> mapEventToState(CounterEvent event) async* {
PointState currentState = PointState(state.x, state.y, state.z);
if (event is ButtonPressedReduce) {
currentState.x = state.x - 1;
currentState.y = event.point.y;
currentState.z = state.z - 1;
yield currentState;
} else if (event is ButtonPressedAdd) {
currentState.x = state.x + 1;
currentState.y = event.point.y;
currentState.z = state.z + 1;
yield currentState;
//重置状态
} else if (event is ButtonPressReset) {
yield PointState.reset();
//单点位数值增加
} else if (event is ButtonPressAddX) {
yield state.update(event.x, currentState.y, currentState.z);
}
}
}
使用BlocProvider在需要该Bloc的Widget Tree上进行包裹,这里直接放到了整个页面上,可以根据需要放到适当的Widget Tree层级上,同时这里的BlocProvider也自动处理了CounterBloc的关闭操作,所以我们不必使用一个StatefulWidget。
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: BlocProvider<CounterBloc>(
create: (context) => CounterBloc(),
child: CounterPage(),
),
);
}
}
然后就是在页面中使用BlocBuilder,我们通过扩展函数的形式或者使用BlocProvider.of(context)获取CounterBloc,然后在builder函数中,使用当前的State对象值count,并通过counterBloc对象进行事件Event变化的操作:
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//这里也可以使用扩展函数的形式 二选一
final CounterBloc counterBloc = context.bloc<BlocA>()
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.add(CounterEvent.increment);
},
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () {
counterBloc.add(CounterEvent.decrement);
},
),
),
],
),
);
}
}
官方例子的全部代码见:
https://github.com/felangel/bloc/blob/master/packages/flutter_bloc/example/lib/main.dart
在例子中还使用了Bloc来控制主题theme实现darkMode的切换。
两个关于BlocListener的使用例子:
https://bloclibrary.dev/#/recipesfluttershowsnackbar
https://bloclibrary.dev/#/recipesflutternavigation
如果要在State变化时,除了更新UI之外还要做一些其他的事情,那么这时候就可以使用BlocListener,BlocListener包含了一个listener用以做除UI更新之外的事情,该逻辑不能放到BlocBuilder里的builder中,因为这个方法会被Flutter框架调用多次,builder方法应该只是一个返回Widget的函数。
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
final dataBloc = BlocProvider.of<DataBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: BlocListener<DataBloc, DataState>(
listener: (context, state) {
if (state is Success) {
Scaffold.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.green,
content: Text('Success'),
),
);
}
},
child: BlocBuilder<DataBloc, DataState>(
builder: (context, state) {
if (state is Initial) {
return Center(child: Text('Press the Button'));
}
if (state is Loading) {
return Center(child: CircularProgressIndicator());
}
if (state is Success) {
return Center(child: Text('Success'));
}
},
),
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
dataBloc.add(FetchData());
},
),
],
),
);
}
}
这三者的使用可以见官网介绍:
https://bloclibrary.dev/#/flutterbloccoreconcepts
https://pub.dev/packages/flutter_bloc
Bloc Code Generator可以方便的生成构建Bloc的模版代码,在项目工程上右键,New -> New Bloc -> Generate New Bloc
一些命名State和Event的公约:https://bloclibrary.dev/#/blocnamingconventions
简要介绍一下和provider的关系:
provider被设计用于依赖项注入(其通过包装InheritedWidget实现)。您仍然需要弄清楚如何管理状态(通过ChangeNotifier,Bloc,Mobx等)。
Bloc库在内部使用provider以轻松在整个Widget 树中提供和访问bloc。
首先BlocProvider继承了SingleChildStatelessWidget, 复写的buildWithChild中返回了一个InheritedProvider,而InheritedProvider就是provider库中的API,
class BlocProvider<T extends Bloc<dynamic, dynamic>>
extends SingleChildStatelessWidget with BlocProviderSingleChildWidget {
...
@override
Widget buildWithChild(BuildContext context, Widget child) {
return InheritedProvider<T>(
create: _create,
dispose: _dispose,
child: child,
lazy: lazy,
);
}
...
}
参考:https://bloclibrary.dev/#/faqs
首先上面的例子中Bloc的使用都局限在一个页面中 ,那么在多个页面之间甚至是整个app中,如何共享同一个Bloc呢,这里可以参考:https://bloclibrary.dev/#/recipesflutterblocaccess
参考:
Flutter Architecture Samples - Brian Egan
Flutter Shopping Card Example
Flutter TDD Course - ResoCoder
由于Bloc的设计就是将业务逻辑从UI层中抽离,所以测试也变得更加容易,首线添加测试用的库:
dev_dependencies:
test: ^1.3.0
bloc_test: ^5.0.0
详细步骤参考:https://bloclibrary.dev/#/testing
看一下Bloc的源码,发现它是继承Stream的,而什么是Stream呢,可以看文末的参考一,
abstract class Bloc<Event, State> extends Stream<State> implements Sink<Event> {
final PublishSubject<Event> _eventSubject = PublishSubject<Event>();
BehaviorSubject<State> _stateSubject;
/// Returns the current [state] of the [bloc].
State get state => _stateSubject.value;
/// Returns the [state] before any `events` have been [add]ed.
State get initialState;
/// Returns whether the `Stream` is a broadcast stream.
@override
bool get isBroadcast => _stateSubject.isBroadcast;
/// {@macro bloc}
Bloc() {
_stateSubject = BehaviorSubject<State>.seeded(initialState);
_bindStateSubject();
}
...
void _bindStateSubject() {
Event currentEvent;
transformStates(transformEvents(_eventSubject, (Event event) {
currentEvent = event;
return mapEventToState(currentEvent).handleError(_handleError);
})).forEach(
(State nextState) {
if (state == nextState || _stateSubject.isClosed) return;
final transition = Transition(
currentState: state,
event: currentEvent,
nextState: nextState,
);
try {
BlocSupervisor.delegate.onTransition(this, transition);
onTransition(transition);
_stateSubject.add(nextState);
} on Object catch (error) {
_handleError(error);
}
},
);
}
...
}
同时这里对于Event和State的处理,出现了PublishSubject
这时候RxDart就出场了,原来的Stream流和控制器StreamController的概念,被扩展成了Observable和Subject。
Dart | RxDart |
---|---|
Stream | Observable |
StreamController | Subject |
这里一共有三种类型的Subject,
PublishSubject仅向监听器发送在订阅之后添加到Stream的事件
BehaviorSubject也是一个广播StreamController,它返回一个Observable而不是一个Stream。
与PublishSubject的主要区别在于BehaviorSubject还将最后发送的事件发送给刚刚订阅的监听器。
ReplaySubject也是一个广播StreamController,它返回一个Observable而不是一个Stream。
默认情况下,ReplaySubject将Stream已经发出的所有事件作为第一个事件发送到任何新的监听器。
参考:
Dart | 什么是Stream
Flutter | 状态管理拓展篇——RxDart(四)
Flutter Bloc构建轻量级MVVM
Stream
StreamController
[译]Flutter响应式编程:Streams和BLoC
Reactive Programming - Streams - BLoC