写这篇文字目标是让大家轻松上手BloC模式的开发过程,从了解到应用。掌握BloC应用,理解BloC原理。从Bloc模式的设计原理出发,通过一小时的学习,轻松掌握Bloc模式的项目实践。
在此之前你需要具备以下条件:
(1)会Dart语言,尤其对异步和Stream要有了解。
(2)学过Flutter,使用Flutter开发过单独的页面。
(3)最好是实践过MVP模式,熟悉应用分层设计的过程。
界面编程可以简化为操作触发事件,事件变更状态。
flutter 使用了与很多前端开发框架相同的开发思想,都是声明式编程框架:
状态管理归根到底是对状态数据的管理,在哪里存储、哪里刷新、在哪里修改。
flutter自身已经为我们提供了状态管理,而且你经常都在用到。没错,它就是 Stateful widget。当我们接触到flutter的时候,首先需要了解的就是有些小部件是有状态的,有些则是无状态的。stateless widget 与 stateful widget。在stateful widget中,我们widget的描述信息被放进了State,而stateful widget只是持有一些immutable的数据以及创建它的状态而已。它的所有成员变量都应该是final的,当状态发生变化的时候,我们需要通知视图重新绘制,这个过程就是setState。这看上去很不错,我们改变状态的时候setState一下就可以了。
在我们一开始构建应用的时候,也许很简单,我们这时候可能并不需要状态管理。
但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。
创建子控件的时候设置回调函数,子控件在内部调用该函数,在外部的回调函数做对应状态变更处理。
需要使用单例自己实现,相当于一个全局单例中持有了事件以及订阅者。
每个要用的页面都创建eventBus实例(实际上是同一个实例),调用订阅、通知、移除等方法。
这里订阅参数是回调函数。
通知是一种自下而上的信息传递机制,首先定义继承Notification 的通知类(包含数据),
在需要监听事件的层级节点开始添加NotificationListener<通知类>(onNotification:通知回调,child:结点树)
在回调函数中修改页面整体状态。在子控件中调用通知类.dispatch(context) 即可通知顶层更新界面。
是一种从上而下传递和共享数据的方式,首先定义继承InheritedWidget的共享数据控件,在用到控件的地方
context.dependOnInheritedWidgetOfExactType() 获取数据共享控件,之后就可以读取
其中的数据。数据变更是通过外层操作修改数据来实现。
也需要定义继承ChangeNotifier的数据类,包含要管理的数据和修改和通知数据变更的方法。使用ChangeNotifierProvider《数据类》.value()
来建立管理状态的数据和控件树。在子控件树中使用Provider.of<数据类>(context)来获取共享的数据实体,可以调用其中的方法或使用其中的数据。
Provider 更像是一个依赖注入工具
setState只适用于管理当前Widget中的少量状态。
回调函数的方式让父控件和子控件深度耦合,事件总线的方式集中处理消息,如果所有状态都使用EventBus效率太低。Notification 适用于点击事件的向上传递。InheritedWidget适用从上而下建立数据共享。
每种方案都无法平衡解耦、事件、状态的维护。适合做一些简单事件传递和状态变更。
各种构架MVP, MVVM, MVI, 目的就是数据和逻辑分离, 逻辑和UI分离,
所以初识Flutter的时候对这种万物皆widget, 一个树里面包含一切的方式有点怀疑, UI逻辑数据写成一堆, 程序功能复杂后, 肯定会越写越乱.
设计app的架构经常会引起争论。每个人都有自己喜欢的一套炫酷的架构和一大堆名词。针对flutter应用的设计常见的有BloC、Redux等方案。
分层也好架构设计也罢,目的无非就是把代码各部分职能、组织关系拆分清楚,方便我们的开发和维护。
Flutter带来的一套响应式设计并不能很好的兼容MVC。一个脱胎于这个经典模式的新的架构就出现在了Flutter社区–BloC。
BloC是Business Logic Components的缩写。BloC的哲学就是app里的所有东西都应该被认为是事件流:一部分组件订阅事件,另一部分组件则响应事件。BloC 将event流作为输入,并将它们转换为state流作为输出。
使用BloC的好处
BloC可以比较轻松地将展示层的代码与业务逻辑分开,从而使您的代码快速,易于测试且可重复使用。
Bloc在设计时考虑到了以下三个核心价值:
简单:易于理解,可供技能水平不同的开发人员使用。
强劲:通过将它们组成更小的组件,帮助制作出色而复杂的应用程序。
可测试:轻松测试应用程序的各个方面,以便我们可以自信地进行迭代。
安装流程:
bloc - bloc的核心库
flutter_bloc - 强大的Flutter Widgets可与bloc配合使用,以构建快速,反应灵活的移动端应用程序
(1)在pubspec.yaml中添加bloc 和 flutter_bloc的依赖。
dependencies:
bloc: ^6.0.0
flutter_bloc: ^6.0.0
(2)拉取依赖flutter packages get
(3)引入依赖
在main.dart中添加
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Stream是Dart:async库的核心API,可以接收一系列异步事件。Stream这个单词可以代表小河或小溪,那我们就用一个故事理解这个API。
Stream这个小溪上游会时不时(异步)的有水流下了,每次来多大水、多久来一次咱都不知道。具体为啥咱也不敢说,咱也不敢问。我在小溪上安个(订阅listen)小水车,水(onData)流下来的时候就会转动,考虑到有杂物也会流下来,我给水车加个过滤网(onError)来拦截。
有一天我不在的时候小溪流下来一个冰山,差点把我的水车砸了,这怎么能忍啊!我就沿着小溪上游查看,发现小溪上游都是一个工厂(StreamController),工厂的出水口(controller.stream)是小溪源头,混进厂子里面继续查看,厂子里面挺复杂的,看得我头晕转向,只能朝着有光的地方走。果然欧气爆棚,在厂子里面找到了最终的源头(StreamSink)。这SreamSink就是个水槽,有的厂水槽边上排了一组水桶,按照次序一桶一桶往小溪倒水(从迭代器创建Stream);有的厂的水槽靠送水工倒水,这送水工也很不靠谱,有时候半天不来一次,(从Future创建Stream);还有的厂子把水槽的建的老远了,具体谁往里面倒啥东西,我就不去看了,怕看到不该看的。
我找到厂长把冰山的问题说了下,厂长只给我保证流下去的是水(建厂时指定的泛型类型),冰山也是水,还威胁我下次给我流个更大的冰山下去,让我建个更结实的网子拦着。我是气的原地爆炸,想着在厂子里搞点破坏啥的。最后还真让我找到个大杀器,工厂有个核按钮(controller.close()),但不是原地爆炸的那种,按一下可以一键倒闭。也算设计者良心,闭前会给小溪流个标记(done)下去,让小溪上的水坝知道水厂老板跑路了。想了想还是算了,等哪天幼儿园毕业不玩泥巴了就让你丫的跑路。
算了回家去了,顺便看看别人家小溪上有什么玩法,长长见识。
有的小溪只有一个分叉,所以只能有一个监听,这种流叫单订阅流。
有的小溪比较特殊,可以有很多分叉,每个分叉都可以添加一个监听,而且分叉流下来的东西是相同的,这种流叫广播流。
有的小溪上搭建了很多新设备,比如做简单变换的(map),复杂变换的(transform),筛选的(where),控制流速的(take),这些设备流出的还是小溪,可以随意组合加工,玩法多样。
我还发现当我们创建订阅的时候会返回个遥控器(StreamSubscription),不仅可以通过这个遥控器设置事件处理,还有暂停(pause)、恢复(resume)和取消(cancel)键。等我不想玩的时候暂停就好,再也不用担心没人在的时候水车被冲垮了,想玩的时候按下恢复就行,退出的是按下cancel就可以取消订阅。
另一种生成流的方式是生成器,async * 标记的方法都是生成器,返回值只能是Stream类型,方法内部每yield一次都会向返回的流中添加一次数据。
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
yield i;
}
}
Cubit是一种特殊的Stream,用来维护UI的状态,是Bloc模式的基石。通常我们会用一个变量来标记UI的状态,还要设计一些方法来控制这个状态的变化,状态变化后还得等界面刷新的时候更新UI。
Cubit将这些结合在一起了,可以把Cubit当做一类状态的集合,你调用他的方法就会改变Cubit当前的状态,状态对应控件所需展示的数据都在状态中。如果状态对应复杂的数据结构,可以自定义状态类型。甚至可以定义一个基类,然后用不同的子类代表不同的状态,每个子类内部可以有各自不同的数据结构。
我们从一个int变量来表示状态的案例讲起。
首先Cubit维护的泛型类型就对应原本状态标记的类型,通过继承Cubit,并通过构造函数来设置初始状态。
class CounterCubit extends Cubit<int> {
CounterCubit(int initialState) : super(initialState);
}
final cubitA = CounterCubit(10); // 状态从 10 开始
在需要更新状态的控件中,通过cubitA.state就可以拿到状态值用于显示了。
在子类Cubit外部暴露的业务方法中可以通过emit方法来实现修改状态。
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
在按钮的点击事件中调用cubitA.increment() 就修改了状态值。纳尼,这么用和直接使用一个int变量有什么不同?
onPressed: () {
setState(() {
_cubitA.increment();
});
},
这其实已经将页面中的一部分逻辑转移到Cubit中了,页面中不存在修改数据的逻辑了。其次,如果这个业务逻辑是要请求网络的,那这部分代码也可以从页面中剥离出来,页面只需要处理显示和事件响应就行了。而且状态的变化是流来实现的,完全是异步的。
注意:Bloc 和 Cubits 都会忽略重复的状态。
Cubit还可以通过覆盖Cubit中onChange 方法来记录和观察状态的变更,每次状态变更都会调用该方法,入参Change不仅包含了当前的状态,还包含了变化前的状态,在这里打日志可以很方便的追踪状态的变更。
@override
void onChange(Change<int> change) {
print(change);// 打印日志
super.onChange(change);
}
在进行业务处理时,如果出现错误的情况,可以通过addError方法来创建状态。
void increase() {
if (this.state > 10) {
addError("计数不能大于10!", StackTrace.current);
} else {
this.emit(this.state + 1);
}
}
同样可以覆写onError方法来打印错误日志。
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
如果你理解了Cubit那么对Bloc也会很容易理解,因为Bloc就是Cubit的子类,主要的变化是触发状态变更的不再是方法调用,而是事件的传递。
这么做好处是,可以更清晰的记录是什么事件触发了状态A到状态B的变更。
事件是根据界面的交互来定义的,不同的操作即便会得到相同的状态也得定义不同的事件对象,如果事件需要传业务数据,那么就需要定义一个事件基本类型,在不同事件子类中添加字段进行数据传递。如果只需要标记不同事件类型,那么用枚举或者基本变量也行的。事件也有可能是与Bloc交互的其他层来产生,比如来自数据层的事件。
此处针对++按钮和–按钮点击,定义一个increase和decrease事件。
enum CounterEvent {
increase,decrease}
或者定义子类也可以
@immutable
abstract class CountEvent {
}
class IncreaseCountEvent extends CountEvent{
}
class DecreaseCountEvent extends CountEvent{
}
定义Bloc是需要指定两个泛型参数,第一个是事件类型,第二个是状态类型,构造函数中同样需要指定初始状态,最后需要实现mapEventToState来实现事件到状态的转变,只需要在其中处理好event,根据业务逻辑yield对应的状态即可。
class CountBloc extends Bloc<CountEvent, int> {
CountBloc() : super(0);
@override
Stream<int> mapEventToState(
CountEvent event,
) async* {
if (event is IncreaseCountEvent) {
yield state + 1;
} else if (event is DecreaseCountEvent) {
yield state - 1;
}
}
}
现阶段而言使用Bloc和Cubit的流程是一样的,首先在页面中定义Bloc实例。
final _countBloc = CountBloc();
在需要显示数据的地方使用当前状态
Text("${_countBloc.state}",),
有区别的地方是之前调用cubit方法的地方现在变成通过bloc实例add事件来实现。
onPressed: () {
setState(() {
_countBloc.add(IncreaseCountEvent());
});
},
Bloc 是Cubit的子类,同样具有onChange 和onError回调方法,用法和之前一样就不说了。Bloc新增了onTransition方法,可以在覆写这个方法来打印事件和状态的变更。
@override
void onTransition(Transition<CountEvent, int> transition) {
print(transition);
super.onTransition(transition);
}
Bloc中有一个BlocObserver 类型的静态变量,
static BlocObserver observer = BlocObserver();
这里的BlocObserver 是框架提供给全局事件观察的接口,里面有如下方法,会在事件的状态变更的过程中被调用。
class BlocObserver {
void onCreate(Cubit cubit) {
}
void onEvent(Bloc bloc, Object event) {
}
void onChange(Cubit cubit, Change change) {
}
void onTransition(Bloc bloc, Transition transition) {
}
void onError(Cubit cubit, Object error, StackTrace stackTrace) {
}
void onClose(Cubit cubit) {
}
}
我们可以继承BlocObserver 来覆写其中的部分方法,进行日志打印。
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(Cubit cubit, Change change) {
print('${cubit.runtimeType} $change');
super.onChange(cubit, change);
}
@override
void onTransition(Bloc bloc, Transition transition) {
print('${bloc.runtimeType} $transition');
super.onTransition(bloc, transition);
}
@override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {
print('${cubit.runtimeType} $error $stackTrace');
super.onError(cubit, error, stackTrace);
}
}
然后在main()方法中添加Bloc.observer = SimpleBlocObserver();
即可在全局打印日志。
Transition {
currentState: 0, event: Instance of 'IncreaseCountEvent', nextState: 1 }
CountBloc Transition {
currentState: 0, event: Instance of 'IncreaseCountEvent', nextState: 1 }
Change {
currentState: 0, nextState: 1 }
CountBloc Change {
currentState: 0, nextState: 1 }
注意: 这里onTransition 比 onChange 先执行,同名方法在Bloc中覆写的比全局的观察者先执行。
上面的Bloc已经实现了将业务逻辑剥离到Bloc中,但仍旧没法实现进一步的解耦。而且使用上很不方便。
比如:要在子Widget中使用Bloc实例,那就得想办法把bloc传递到子控件中。这需要修改子控件实现,无形中又增强了UI层和业务层的耦合关系。
此外即便是使用了bloc库,也面临着子控件中触发的事件,需要传递事件到父控件中进行setState 操作。
要解决视图层对Bloc实例代码上的依赖,我们需要更灵活的依赖注入。
Flutter中有个InheritedWidget组件,可以顶层创建共享数据,子组件中可以直接通过context拿到共享的数据。
flutter_bloc这个库就是为了方便在flutter应用开发中更方便的使用bloc模式而进行设计的。
BlocProvider 是Flutter部件(widget),可通过BlocProvider.of (context)向其子级提bloc。它被作为依赖项注入(DI)部件(widget),以便可以将一个bloc的单个实例提供给子树中的多个部件(widgets)。
BlocProvider的使用就两个要点:
一是在页面找到合适的位置创建Bloc,一般是在APP页面的home节点开始创建。这样做的好处是在后续子结点中所有控件都可以拿到该Bloc实例。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: BlocProvider(
create: (context) => CountBloc(),
child: MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}
二是在后续界面读取状态值,通过如下方式可以拿到state数据。
Text("${BlocProvider.of(context).state}"),
如果想要向Bloc添加事件,同样也得先获取Bloc实例,再调用add方法。
onPressed: () {
setState(() {
BlocProvider.of<CountBloc>(context).add(IncreaseCountEvent()
});
},
通过使用BlocProvider可以不用在页面中创建Bloc实例成员变量了,子控件中也可以通过依赖注入拿到Bloc实例,但向Bloc添加事件的时候仍然需要setState才可以生效,而且页面中调用该方法会重新build整个页面。如果每次我只想重新构建状态变更影响的那一个控件该怎么处理呢?
BlocBuilder 就是用于依据状态来重新构建Widget的控件。首先BlocBuilder本身就是一个StatefulWidget,它可以根据状态来构建不同的子视图,最终实现局部刷新的目的。
使用BlocBuilder要泛型指定绑定的Bloc类型和State类型,然后在构造函数中创建builder,builder是一个函数类型,传入的参数有context和当前的状态。
如下是将之前布局中的Text控件使用BlocBuilder来包装。
children: <Widget>[
BlocBuilder<CountBloc, int>(
builder: (context, state) {
return Text("${state}");
},
buildWhen: (previousState, currentState) {
if (previousState + currentState > 10) {
return false;
} else {
return true;
}
},
),
],
BlocBuilder的构造函数中还有一个buildWhen参数,这个函数有两个参数,之前的状态和当前的状态,可以根据前后状态的差异来决定本次是否重新构建,只有当返回true时才会调用builder。
有了BlocBuilder之后在添加事件的时候就无需使用setState了。
onPressed: () {
BlocProvider.of<CountBloc>(context).add(IncreaseCountEvent());
},
在APP开发过程中不仅有数据的展示,也有一些在页面层级之上调用一些函数的功能,比如Toast、Dialog、SnackBar、导航跳转等。这些都是依赖页面整体的功能,而不是和具体某个控件相关。
BlocListener 就是应对这种场景的控件,一般是在当前页面的body部分创建,比如将BlocListener作为Scaffold的body结点。创建BlocListener 同样也要指定监听的Bloc类型和状态类型,其次需要定义listener方法,Bloc的每个状态变更都会调用该方法。将之前的页面根节点作为child参数。
return Scaffold(
body: BlocListener<CountBloc, int>(
listener: (context, state) {
if (state > 10) {
Scaffold.of(context).showSnackBar(SnackBar(content: Text("当前数量已经大于10!!"),));
}
},
child: Center(。。。。。)
);
同样BlocListener 也有listenWhen参数,根据前后状态来决定是否触发listener。
之前开发过APP的同学可能用过Repository模式,就是将APP中的数据需求抽取成Repository接口,可以根据不同的数据源来定义不同的实现,比如LocalRepository和NetRepository,通过代理来得到最终的Repository实现。通常会把Repository作为单例来让全局共享数据。数据缓存本来就是很占内存的,如果使用单例会导致内存利用效率不高。
Bloc也对数据的共享也做了支持,可以更好的控制数据仓库的生命周期。RepositoryProvider就是这样的控件,可以将存储库的单个实例提供给子树中的多个部件(widgets)。RepositoryProvider不需要指定泛型类型,只用指定create方法和child结点即可。
home: RepositoryProvider(
create: (context) => RepositoryA(context),
child: BlocProvider(
create: (context) => CountBloc(),
child: MyHomePage(title: 'Flutter Demo Home Page'),
),
),
当需要在Bloc中使用Repository是可以按如下方式,值得注意的是,这要求Bloc实例中要能拿到context。
RepositoryProvider.of<RepositoryA>(context)
上面的案例界面比较简单,维护的状态也比较单一。如果一个页面有多个Bloc,那么在创建Bloc的时候只能通过嵌套BlocProvider来实现,MultiBlocProvider 就是用于将创建多个Bloc的过程合并在一起。
MultiBlocProvider(
providers: [
BlocProvider<BlocA>(
create: (BuildContext context) => BlocA(),
),
BlocProvider<BlocB>(
create: (BuildContext context) => BlocB(),
),
BlocProvider<BlocC>(
create: (BuildContext context) => BlocC(),
),
],
child: ChildA(),
)
MultiBlocListener是为了将多个BlocListener合并在一起而设计的,同样无需嵌套即可合并。
MultiBlocListener(
listeners: [
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
},
),
BlocListener<BlocB, BlocBState>(
listener: (context, state) {
},
),
BlocListener<BlocC, BlocCState>(
listener: (context, state) {
},
),
],
child: ChildA(),
)
MultiRepositoryProvider 也是为了方便合并多个Repository时创建的,用法和其它Muilt控件一致。
MultiRepositoryProvider(
providers: [
RepositoryProvider<RepositoryA>(
create: (context) => RepositoryA(),
),
RepositoryProvider<RepositoryB>(
create: (context) => RepositoryB(),
),
RepositoryProvider<RepositoryC>(
create: (context) => RepositoryC(),
),
],
child: ChildA(),
)
BlocConsumer类似于BlocListener和BlocBuilder的组合,用于一个状态改变既要重绘UI也要提示Toast或执行一些操作的场景。
BlocConsumer<BlocA, BlocAState>(
listenWhen: (previous, current) {
},
listener: (context, state) {
},
buildWhen: (previous, current) {
},
builder: (context, state) {
}
)
使用Bloc开发应用首先要进行分层设计,类似于MPV模式。同样分为三层,表现层(Presentation)、业务逻辑(Business Logic)、数据层(Data)。
(1)数据层
首先定义基础的数据类型,创建数据表或者网络请求的数据结构,在Repository中创建围绕这些数据结构增删改查的方法,为业务逻辑层提供可调用的方法。
class Repository {
final DataProviderA dataProviderA;
final DataProviderB dataProviderB;
Future<Data> getAllDataThatMeetsRequirements() async {
final RawDataA dataSetA = await dataProviderA.readData();
final RawDataB dataSetB = await dataProviderB.readData();
final Data filteredData = _filterData(dataSetA, dataSetB);
return filteredData;
}
}
产出app_repository.dart
(2)业务逻辑层
定义业务逻辑层的过程类似于MVP模式中定义View和Presenter接口方法的过程。MVP中我们会分析用户在View层的操作,每个操作会触发什么样的逻辑处理,以此定义P层的接口方法。然后根据每个操作之后界面该有什么样的响应,定义View层的接口方法,供业务处理后由P层调用。
针对一个Bloc,要分析这个Bloc负责的视图控件部分在用户操作时会触发什么Event,每个Event要传递什么数据。考虑每个Event需要如何处理数据,最终产生什么样的State,界面展示State需要更新哪些数据。
然后开始定义Event。
创建app_event.dart ,定义基类Event,创建继承该基类的子类事件。推荐子类命名使用过去时,因为从bloc的角度来看,事件是已经发生的事情。动词过去时+Event后缀。
创建app_state.dart,定义基类状态,创建继承该基类的子类状态,状态命名推荐使用名词+State后缀。
创建app_bloc.dart,定义继承Bloc的子类,需要指定Event和State泛型类型,实现mapEventToState方法。
注意:根据页面交互的复杂度,可以考虑定义一至多个Bloc。Bloc也是Stream,
针对多个bloc有状态联动时,可以在一个Bloc中监听另一个Bloc的State变化。
class MyBloc extends Bloc {
final OtherBloc otherBloc;
StreamSubscription otherBlocSubscription;
MyBloc(this.otherBloc) {
// 在通过构造函数传入依赖的其他bloc
otherBlocSubscription = otherBloc.listen((state) {
// 处理state,并通过add()向当前bloc添加事件。 });
}
@override
Future<void> close() {
// 在当前Bloc关闭的时候停止订阅。
otherBlocSubscription.cancel();
return super.close();
}
}
(3)表现层
表现层就是UI层,这里就是flutter_bloc这个包的天下了。先考虑在什么位置创建bloc,如果bloc和页面的生命周期一致,那么在创建Scaffold的时候调用BlocProvider创建Bloc实例就行。
其次需要考虑状态变化时要更新哪部分界面,变化的部分用BlocBuilder包装就好,还可以通过buildWhen来精细化的控制更新过程。
最后针对需要全局展示的内容或调用的方法,可以在创建BlocProvider 的子结点上创建BlocListener,处理Dialog、Toast、Snackbar的展会。
todo 项目案例