前言
Flutter和SwiftUI是声明式和响应式编程在移动端的典范, 使用它们开发复杂业务, 状态管理是绕不过去的话题.
BLoC【Business Logic Component】设计模式是paolo soares
和 cong hui
在2018年Google dartconf
上提出的,具体设计思想和于传统开发方式的比较, 可以参考YouTube演示视频
BloC设计模式是Flutter解决状态管理的一种方案, 今天笔者将使用bloc (实现该设计模式且比较流行的库)来构建一个工程中完整且常见的页面, 作为Flutter初学者, 希望以此和大家交流, 共同进步, 你可以在这里 找到该项目的完整代码~
BLoC 为什么选用BLoC ?
BLoC将视图层与业务逻辑分离变得容易,从而使代码简单 ,可测 并且可重用 , 更重要的是它以一种数据驱动的规范化流程来编写, 各个层级各司其职, 清晰分明
如果你是Web开发者, 你肯定熟悉React+Redux、如果你是iOS开发者, 你可能会使用过RxSwift+ReactorKit, 如果你熟悉单项数据流开发模式, 那么你对BLoC就非常容易上手, BLoC就是用reactive programming方式构建应用, 一个由流构成的完全异步的世界。从而达到界面与业务分离的逻辑。
通过bloc官网 介绍的流程图来看, 它将应用分为了三层: 展示层, 逻辑层, 数据层. 而通过流程来看, bloc逻辑层作为三层的中枢, 处理三个层级之间的交流.
并且我们可以根据官方的命名规范来组织各个层级结构:
数据层(Data Layer): 职责是从一个或多个源中检索/处理数据。
使用DataProvider
来提供原始数据(eg: CRUD等)
使用Repository
来包装一个或多个DataProvider
, 然后与bloc进行通讯
逻辑层(Business Logic Layer): 职责是接收表示层的事件输入, 请求检索数据, 进而以数据建立表示层的新状态
多个bloc之间也可以进行通讯: 每个bloc都有一个状态流, , 其他bloc可以订阅该流, 以便对自己bloc内部做出新状态的改变
展示层(Presentation Layer): 职责是向bloc发送用户事件, 根据一个或多个bloc的状态来进行渲染改变
示例 需求 在大家了解过bloc widgets 的api之后, 那就跟着我一起进行页面的开发吧😸
这里我已经准备好了页面所需的数据来源了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 { "code" : 0 , "errMsg" : "操作成功" , "data" : { "time" : { "start_time" : "21:00" , "end_time" : "22:00" }, "address" : "北京" , "types" : [{ "name" : "普通公寓:住宅用地,公寓产权,70年,户型面积小" , "value" : "1" , "selected" : 2 }, { "name" : "商务公寓:商业用地,商务公寓产权,40年" , "value" : "2" , "selected" : 1 }, { "name" : "酒店式公寓:商业用地,商务公寓或公寓产权,40年,面积较小" , "value" : "3" , "selected" : 2 }], "count" : "3" } }
简单梳理下页面的需求:
根据json数据, 展示页面的上的内容
点击提交将修改后的内容提交至服务器, 提交参数为:start_time
, end_time
, address
, count
和选中的type
对应的value值
并且在点击页面返回的时候, 需要校验当然页面是否有修改过的数据, 并对用户进行弹窗展示
数据 定义展示模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class FormModel { Time time; String address; List <SelectValueItem> types; String count; get timeString => '${time.start} -${time.end} ' ; get counString => '${count} 次' ; FormSubmit toSubmit() { return FormSubmit( start: time.start, end: time.end, address: address, type: types.firstWhere((e) => e.selected, orElse: null )?.value ?? '0' , count: count, ); } }
定义提交模型
1 2 3 4 5 6 7 8 9 10 11 12 13 class FormSubmit extends Equatable { final String start; final String end; final String address; final String type; final String count; }
模拟网络请求(FormRepository)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const _delay = Duration (milliseconds: 300 );Future<void > wait() => Future.delayed(_delay); class FormRepository { Future<FormModel> fetchFormData() async { await wait(); String jsonString = await rootBundle.loadString("assets/json/form_data.json" ); final jsonResult = json.decode(jsonString); FormModel model = FormModel.fromJson(jsonResult['data' ]); return model; } Future<bool > saveForm(FormSubmit model) async { await wait(); return true ; } }
展示 展示数据的逻辑, 我们定义一个form_bloc处理, 为了编写bloc模板, Bloc Code Generator 插件可以方便的生成构建Bloc的模版代码, 有兴趣的同学可以尝试使用
定义Event:
1 2 3 4 @immutable abstract class FormEvent {}class FormDataRequestEvent extends FormEvent {}
定义State:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @immutable abstract class FormDataState {}class FormInitState extends FormDataState {}class FormLoadedState extends FormDataState { final FormModel data; FormLoadedState({this .data}); } class FormFailureState extends FormDataState { final String error; FormFailureState({@required this .error}); } class FormInProgressState extends FormDataState {}
定义bloc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class FormBloc extends Bloc <FormEvent , FormDataState > { @override FormDataState get initialState => FormInitState(); final FormRepository repo; FormBloc({@required this .repo}) : assert (repo != null ); @override Stream<FormDataState> mapEventToState( FormEvent event, ) async * { if (event is FormDataRequestEvent) { yield FormInProgressState(); try { final model = await repo.fetchFormData(); yield FormLoadedState(data: model); } catch (error) { yield FormFailureState(error: error.toString()); } } }
展示数据: (在页面build的时候, 就可以直接发送请求事件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class FormPage extends StatelessWidget { final repo = FormRepository(); @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider<FormBloc>( create: (ctx) => FormBloc(repo: repo)..add(FormDataRequestEvent()), ), ], child: FormContent(), ); } } body: Container( color: Color(0xFFF6FAFF ), child: BlocConsumer<FormBloc, FormDataState>( listener: (context, state) { if (state is FormFailureState) { ToastUtil.show (state.error); } }, builder: (context, state) { if (state is FormInProgressState) { return LoadingIndicator(); } if (state is FormLoadedState) { var model = state.data; return SingleChildScrollView( child: Column( children: <Widget>[ _sectionHeader(), ContentItem(title: '选择区间' , content: model.timeString, hasArrow: true ), _divider(), ContentItem(title: '选择地址' , content: model.address, hasArrow: false ,), _sectionHeader(), TypeItem(items: model.types ?? [], onIndexTap: (index) => _setType(index),), _divider(), ContentItem(title: '选择次数' , content: model.counString, hasArrow: false ,), _buildSaveButton(), ], ), ); } return Container(); }, ), ),
更改 我们在👆上面的展示的图中可以看到, 选择区间, 更改选择类型, 这些都是需要更改数据源来进行UI更改的, 根据单项数据流模式 , 数据都应由bloc来进行管理, UI层面只进行响应式的更新
所以, 我们最好不要在UI层面来操作数据源, 而是发送选择区间
,选择类型
的事件, 来让bloc进行数据源状态的改变
定义两个Event:
1 2 3 4 5 6 7 8 9 10 11 12 class FormUpdateTimeEvent extends FormEvent { final String start; final String end; FormUpdateTimeEvent({this .start, this .end}); } class FormUpdateTypeEvent extends FormEvent { final int selectIndex; FormUpdateTypeEvent({@required this .selectIndex}); }
更改相应的State (更改后, bloc获取到的state中的data为最新的数据源)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Stream<FormDataState> mapEventToState( FormEvent event, ) async * { if (event is FormUpdateTypeEvent) { if (state is FormLoadedState) { var model = (state as FormLoadedState).data; model.types.forEach((e) => e.selected = false ); model.types[event.selectIndex].selected = true ; yield (state as FormLoadedState).copyWith(data: model); } } if (event is FormUpdateTimeEvent) { if (state is FormLoadedState) { var model = (state as FormLoadedState).data; model.time.start = event.start; model.time.end = event.end; yield (state as FormLoadedState).copyWith(data: model); } }
那么我们只需要在页面UI层面, 发送定义的这两个事件即可
1 2 3 4 5 6 7 8 void _setTime(String start, String end) { BlocProvider.of<FormBloc>(context).add(FormUpdateTimeEvent(start: start, end: end)); } void _setType(int index) { BlocProvider.of<FormBloc>(context).add(FormUpdateTypeEvent(selectIndex: index)); }
总结以上完成的显示和更改, 我们用的一个bloc和对应的多个状态, 其实我们也可以将所有逻辑包装成一个页面的state, 将所有的数据包含在一个state中, 所有的Event对应的State都为同一个, 那么事件的处理一般都为yeild state.copyWith(..)
伪代码如这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 enum DataState { init, loading, loaded, fail, } class FormPageDataState extends FormDataState { FormModel data, FormSubmit submitData, DataState dataState, ... FormPageDataState copyWith({ FormModel data, FormSubmit submitData, DataState dataState, }) { return FormLoadedState( data: data ?? this .data, submitData: submitData ?? this .submitData, dataState: dataState ?? this .dataState, ); }
提交 提交这部分逻辑 , 我决定使用另一个bloc来处理逻辑submit_bloc
, 我们会思考提交的状态有什么:
默认状态, 提交请求中, 请求成功, 请求失败
咦 ?? 那所有的请求几乎都是这样, 那我们不如定义一个普通情况下都通用的状态State:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @immutable abstract class NetworkState {}class NetworkInitState extends NetworkState {}class NetworkSuccessState <T > extends NetworkState { final T data; NetworkSuccessState({this .data}); } class NetworkFailureState extends NetworkState { final String error; NetworkFailureState({@required this .error}); } class NetworkInProgressState extends NetworkState {}
我们来看下提交的Bloc, 是不是简单多了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class SubmitEvent { SubmitEvent({this .submit}); FormSubmit submit; } class SubmitBloc extends Bloc <SubmitEvent , NetworkState > { @override NetworkState get initialState => NetworkInitState(); final FormRepository repo; SubmitBloc({@required this .repo}) : assert (repo != null ); @override Stream<NetworkState> mapEventToState( SubmitEvent event, ) async * { yield NetworkInProgressState(); try { bool result = await repo.saveForm(event.submit); yield NetworkSuccessState<bool >(data: result); } catch (error) { yield NetworkFailureState(error: error.toString()); } } }
提交现在还有一个小问题: 如果用户快速点击提交按钮
多次, 就会重复提交多次, 这不是我们愿意看到的; 所以我们可以通过节流(throttle)
的方式来避免, 引入rxdart
1 2 3 4 5 6 7 8 9 @override Stream<Transition<SubmitEvent, NetworkState>> transformEvents(Stream<SubmitEvent> events, transitionFn) { return super .transformEvents(events.throttleTime(Duration (milliseconds: 500 )), transitionFn); }
校验 点击返回按钮的时候, 我们需要判断需要提交的数据是否变化了, 我们在该页面请求成功的State中, 添加一个初始化提交模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class FormLoadedState extends FormDataState { final FormModel data; final FormSubmit initSubmit; FormLoadedState({this .data, this .initSubmit}); FormLoadedState copyWith({ FormModel data, FormSubmit initSubmitModel, }) { return FormLoadedState( data: data ?? this .data, initSubmit: initSubmitModel ?? this .initSubmit, ); } } if (event is FormDataRequestEvent) { yield FormInProgressState(); try { final model = await repo.fetchFormData(); yield FormLoadedState(data: model, initSubmit: model.toSubmit()); } catch (error) { yield FormFailureState(error: error.toString()); } }
我们看下UI层的事件处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Widget build(BuildContext context) { return WillPopScope( onWillPop: () => _exitPage(context), child: ... ) } Future<bool > _exitPage(BuildContext context) async { var state = BlocProvider.of<FormBloc>(context).state; if (state is FormLoadedState) { if (state.data.toSubmit() != state.initSubmit) { JLDialog.cancelConfirmTextDialog(context, content: '内容已经变更,是否保存?' ).then((confirm) { confirm ? _confirmSave() : Navigator.of(context).pop(); }); return false ; } } return true ; }
完整示例代码在这里
参考 https://github.com/felangel/bloc
https://github.com/4seer/openflutterecommerceapp