Flutter 架构:Provider vs BLoC
已发表: 2020-04-17使用 Flutter 编写应用程序为选择架构创造了巨大的机会。 通常情况下,“我应该选择哪一个?”这个问题的最佳答案。 是“视情况而定”。 当你得到这个答案时,你可以确定你找到了一位编程专家。
在本文中,我们将浏览移动应用程序中最流行的屏幕,并在两种最流行的 Flutter 架构中实现它们: Provider和BLoC 。 因此,我们将了解每种解决方案的优缺点,这将帮助我们为下一个模块或应用程序选择正确的 Flutter 架构。
Flutter架构简介
为 Flutter 开发项目选择架构非常重要,这主要是因为我们正在处理一种不太常用的声明式编程范式。 这完全改变了原生 Android 或 iOS 开发人员熟悉的管理状态的方法,即强制编写代码。 在应用程序的一个地方可用的数据在另一个地方并不那么容易获得。 我们没有直接引用树中的其他视图,我们可以从中获得它们的当前状态。
Flutter 中的 Provider 是什么
顾名思义, Provider是一种Flutter架构,将当前的数据模型提供到我们当前需要的地方。 它包含一些数据并在发生更改时通知观察者。 在 Flutter SDK 中,这种类型称为ChangeNotifier 。 为了使ChangeNotifier类型的对象可用于其他小部件,我们需要ChangeNotifierProvider 。 它为其所有后代提供观察对象。 能够接收当前数据的对象是Consumer ,它在其构建函数的参数中有一个ChangeNotifier实例,可用于为后续视图提供数据。
Flutter 中的 BLoC 是什么
业务逻辑组件是一种 Flutter 架构,与移动领域的流行解决方案(如 MVP 或 MVVM)更相似。 它提供了表示层与业务逻辑规则的分离。 这是 Flutter 强烈强调的声明式方法的直接应用,即UI = f (state) 。 BLoC 是来自用户界面的事件所在的地方。 在这一层中,由于将业务规则应用于给定事件,BLoC 以特定状态响应,然后返回到 UI。 当视图层接收到一个新状态时,它会根据当前状态的需要重建它的视图。
对 Flutter 开发感到好奇?
查看我们的解决方案如何在 Flutter 中创建列表
可滚动列表可能是移动应用程序中最流行的视图之一。 因此,在这里选择正确的 Flutter 架构可能至关重要。 从理论上讲,显示列表本身并不困难。 例如,当我们添加对每个元素执行特定操作的能力时,情况会变得更加棘手。 这应该会导致应用程序中不同位置的变化。 在我们的列表中,我们将能够选择每个元素,并且每个选定的元素都将显示在不同屏幕上的单独列表中。

因此,我们必须存储已选择的元素,以便它们可以显示在新屏幕上。 此外,每次点击复选框时,我们都需要重建视图,以实际显示选中/取消选中。
列表项模型看起来很简单:
类社交媒体{ 内部标识; 字符串标题; 字符串图标资产; 布尔是最喜欢的; 社交媒体( {@required this.id, @required this.title, @required this.iconAsset, this.isFavourite = false}); 无效 setFavourite(bool isFavourite) { this.isFavourite = isFavourite; } }
如何使用 Provider 创建列表
在提供者模式中,上述模型必须存储在一个对象中。 该对象应扩展ChangeNotifier以便能够从应用程序中的另一个位置访问SocialMedia 。
类 SocialMediaModel 扩展 ChangeNotifier { final List<SocialMedia> _socialMedia = [ /* 一些社交媒体对象 */ ]; UnmodifiableListView<SocialMedia> 获得收藏{ return UnmodifiableListView(_socialMedia.where((item) => item.isFavourite)); } UnmodifiableListView<SocialMedia> 全部获取 { 返回 UnmodifiableListView(_socialMedia); } 无效 setFavourite(int itemId, bool isChecked) { _社交媒体 .firstWhere((item) => item.id == itemId) .setFavourite(isChecked); 通知监听器(); }
此对象中需要在视图上重建的任何更改都必须使用notifyListeners() 发出信号。 在setFavourite()方法指示 Flutter 重新渲染 UI 片段的情况下,它将观察此对象的变化。
现在我们可以继续创建列表。 要使用元素填充ListView ,我们需要访问SocialMediaModel对象,该对象存储所有元素的列表。 你可以通过两种方式做到这一点:
- Provider.of<ModelType>(上下文,听:假)
- 消费者
第一个提供观察到的对象,并允许我们决定对对象执行的操作是否应该重建当前小部件,使用监听参数。 这种行为在我们的例子中很有用。
类 SocialMediaListScreen 扩展 StatelessWidget { 社交媒体列表屏幕(); @覆盖 小部件构建(BuildContext 上下文){ var socialMedia = Provider.of<SocialMediaModel>(上下文,听:假); 返回列表视图( 儿童:socialMedia.all .map((item) => CheckboxSocialMediaItem(item: item)) .toList(), ); } }
我们需要一份所有社交媒体的列表,但没有必要重新构建整个列表。 让我们看一下列表项小部件的外观。
类 CheckboxSocialMediaItem 扩展 StatelessWidget { 最终社交媒体项目; CheckboxSocialMediaItem({Key key, @required this.item}) : super(key: key); @覆盖 小部件构建(BuildContext 上下文){ 返回填充( 填充:常量 EdgeInsets.all(Dimens.paddingDefault), 孩子:行( 孩子们: [ 消费者<SocialMediaModel>( 建造者:(上下文,模型,孩子){ 返回复选框( 值:item.isFavourite, onChanged: (isChecked) => model.setFavourite(item.id, isChecked), ); }, ), 社交媒体项目( 项目:项目, ) ], ), ); } }
我们监听复选框值的变化,并根据检查状态更新模型。 复选框值本身是使用数据模型中的属性设置的。 这意味着选择后,模型会将isFavourite字段更改为true 。 但是,在我们重建复选框之前,视图不会显示此更改。 在这里,一个Consumer对象提供了帮助。 它提供观察对象并在收到有关模型更改的信息后重建其所有后代。
值得将Consumer仅放置在需要更新小部件的地方,以避免不必要的重建视图。 请注意,例如,如果复选框选择将触发一些额外的操作,例如更改项目的标题,则必须将Consumer在小部件树中移到更高的位置,以便成为负责显示标题的小部件的父级. 否则,标题视图将不会更新。
创建一个最喜欢的社交媒体屏幕看起来很相似。 我们将使用Provider获得最喜欢的项目列表。
类 FavouritesListScreen 扩展 StatelessWidget { 收藏夹列表屏幕(); @覆盖 小部件构建(BuildContext 上下文){ var list = Provider.of<SocialMediaModel>(context, listen: false).favourites; 返回列表视图( 孩子:列表 .map((项目) => 填充( 填充:常量 EdgeInsets.all(Dimens.paddingDefault), 孩子:SocialMediaItem(项目:项目))) .toList(), ); } }
调用build方法时, Provider将返回当前收藏的社交媒体列表。
如何使用 BLoC 创建列表
在我们的简单应用程序中,到目前为止我们有两个屏幕。 它们每个都有自己的BLoC对象。 但是,请记住,主屏幕上的选定项目将出现在最喜欢的社交媒体列表中。 因此,我们必须以某种方式将复选框选择事件转移到屏幕之外。 解决方案是创建一个额外的BLoC对象,该对象将处理影响许多屏幕状态的事件。 我们称之为 global BLoC 。 然后,分配给各个屏幕的BLoC对象将监听全局BLoC状态的变化并做出相应的响应。
在创建BLoC对象之前,您应该首先考虑视图将能够发送哪些事件到 BLoC 层以及它将响应哪些状态。 在全局BLoC的情况下,事件和状态将如下所示:
抽象类 SocialMediaEvent {} 类 CheckboxChecked 扩展 SocialMediaEvent { 最终布尔 isChecked; 最终 int itemId; CheckboxChecked(this.isChecked, this.itemId); } 抽象类 SocialMediaState {} 类 ListPresented 扩展 SocialMediaState { 最终列表<SocialMedia> 列表; ListPresented(this.list); }
CheckboxChecked事件必须在全局BLoC中,因为它会影响许多屏幕的状态——而不仅仅是一个。 当涉及到状态时,我们有一个可以在其中显示列表的状态。 从全局BLoC的角度来看,没有必要创建更多的状态。 两个屏幕都应该显示该列表,并且专用于特定屏幕的各个BLoC应该处理它。 全局BLoC本身的实现将如下所示:
类 SocialMediaBloc 扩展 Bloc<SocialMediaEvent, SocialMediaState> { 最终的 SimpleSocialMediaRepository 存储库; 社交媒体块(this.repository); @覆盖 SocialMediaState 获取初始状态 => ListPresented(repository.getSocialMedia); @覆盖 Stream<SocialMediaState> mapEventToState(SocialMediaEvent 事件) async* { 如果(事件为 CheckboxChecked){ yield _mapCheckboxCheckedToState(event); } } SocialMediaState _mapCheckboxCheckedToState(CheckboxChecked 事件) { final updatedList = (state as ListPresented).list; 更新列表 .firstWhere((item) => item.id == event.itemId) .setFavourite(event.isChecked); 返回列表呈现(更新列表); } }
初始状态是ListPresented——我们假设我们已经从存储库接收到数据。 我们只需要响应一个事件—— CheckboxChecked 。 因此,我们将使用setFavourite方法更新所选元素,并发送包装在ListPresented状态的新列表。
现在我们需要在点击复选框时发送CheckboxChecked事件。 为此,我们需要在可以附加onChanged回调的地方创建一个SocialMediaBloc实例。 我们可以使用BlocProvider获取这个实例——它看起来类似于上面讨论的模式中的Provider 。 要使这样的BlocProvider工作,在小部件树的较高位置,您必须初始化所需的BLoC对象。 在我们的示例中,它将在 main 方法中完成:
void main() => runApp(BlocProvider( 创建:(上下文){ 返回社交媒体块(SimpleSocialMediaRepository()); }, 孩子:ArchitecturesSampleApp()));
多亏了这一点,在主列表代码中,我们可以轻松地使用BlocProvider.of ()调用BLoC并使用add方法向它发送事件:
类 SocialMediaListScreen 扩展 StatefulWidget { _SocialMediaListState createState() => _SocialMediaListState(); } 类 _SocialMediaListState 扩展 State<SocialMediaListScreen> { @覆盖 小部件构建(BuildContext 上下文){ 返回 BlocBuilder<SocialMediaListBloc, SocialMediaListState>( 构建器:(上下文,状态){ 如果(状态为 MainListLoaded){ 返回列表视图( 儿童:state.socialMedia .map((项目) => CheckboxSocialMediaItem( 项目:项目, onCheckboxChanged: (isChecked) => BlocProvider.of<SocialMediaBloc>(上下文) .add(CheckboxChecked(isChecked, item.id)), )) .toList(), ); } 别的 { 返回中心(子:文本(Strings.emptyList)); } }, ); } }
我们已经将CheckboxChecked事件传播到BLoC ,我们也知道BLoC将如何响应此类事件。 但实际上……什么会导致列表在已选中复选框的情况下重建? 全局BLoC不支持更改列表状态,因为它由分配给屏幕的单个BLoC对象处理。 解决方案是前面提到的侦听全局BLoC以更改状态并根据此状态进行响应。 下面,专门用于主要社交媒体列表的BLoC带有一个复选框:
类 SocialMediaListBloc 扩展 Bloc<SocialMediaListEvent, SocialMediaListState> { 最终社交媒体块主块; SocialMediaListBloc({@required this.mainBloc}) { mainBloc.listen((状态){ 如果(状态为 ListPresented){ add(ScreenStart(state.list)); } }); } @覆盖 SocialMediaListState 获取初始状态 => MainListEmpty(); @覆盖 流<SocialMediaListState> mapEventToState( SocialMediaListEvent 事件)异步* { 开关(event.runtimeType){ 案例屏幕开始: yield MainListLoaded((event as ScreenStart).list); 休息; } } }
当SocialMediaBloc返回状态ListPresented时,将通知SocialMediaListBloc 。 请注意, ListPresented传达了一个列表。 它包含有关使用复选框检查项目的更新信息。

同样,我们可以创建一个专门用于收藏夹社交媒体屏幕的BLoC :
类 FavouritesListBloc 扩展 Bloc<FavouritesListEvent, FavouritesListSate> { 最终社交媒体块主块; FavouritesListBloc({@required this.mainBloc}) { mainBloc.listen((状态){ 如果(状态为 ListPresented){ add(FavouritesScreenStart(state.list)); } }); } @覆盖 FavouritesListSate 获取初始状态 => FavouritesListEmpty(); @覆盖 Stream<FavouritesListSate> mapEventToState(FavouritesListEvent 事件) async* { 如果(事件是 FavouritesScreenStart){ var favouritesList = event.list.where((item) => item.isFavourite).toList(); 产生收藏夹列表加载(收藏夹列表); } } }
更改全局BLoC中的状态会导致使用当前列表触发FavouritesScreenStart事件。 然后,过滤掉标记为收藏的项目,并在屏幕上显示这样的列表。
如何在 Flutter 中创建包含多个字段的表单
长表单可能很棘手,尤其是当需求假设不同的验证变体,或者输入文本后屏幕上的一些变化时。 在示例屏幕上,我们有一个由多个字段和“下一步”按钮组成的表单。 在表单完全有效之前,这些字段将自动验证并禁用按钮。 单击按钮后,将打开一个新屏幕,其中包含在表单中输入的数据。
我们必须验证每个字段并检查整个表单更正以正确设置按钮状态。 然后,需要为下一个屏幕存储收集到的数据。

如何使用 Provider 创建具有多个字段的表单
在我们的应用程序中,我们将需要第二个ChangeNotifier ,专用于个人信息屏幕。 因此,我们可以使用MultiProvider ,我们在其中提供ChangeNotifier对象列表。 它们将可供MultiProvider的所有后代使用。
类 ArchitecturesSampleApp 扩展 StatelessWidget { 最终的 SimpleSocialMediaRepository 存储库; ArchitecturesSampleApp({Key key, this.repository}) : super(key: key); @覆盖 小部件构建(BuildContext 上下文){ 返回多提供者( 提供者:[ ChangeNotifierProvider<SocialMediaModel>( 创建:(上下文)=> SocialMediaModel(存储库), ), ChangeNotifierProvider<PersonalDataNotifier>( 创建:(上下文)=> PersonalDataNotifier(), ) ], 孩子:MaterialApp( 标题:Strings.architecturesSampleApp, debugShowCheckedModeBanner:假, 主页:开始屏幕(), 路线:<String, WidgetBuilder>{ Routes.socialMedia: (context) => SocialMediaScreen(), Routes.favourites: (context) => FavouritesScreen(), Routes.personalDataForm: (context) => PersonalDataScreen(), Routes.personalDataInfo: (context) => PersonalDataInfoScreen() }, ), ); } }
在这种情况下, PersonalDataNotifier将充当业务逻辑层——他将验证字段,访问数据模型以进行更新,并更新视图所依赖的字段。
表单本身是来自 Flutter 的一个非常好的 API,我们可以使用属性验证器自动附加验证,并使用onSaved回调将表单中的数据保存到模型中。 我们会将验证规则委托给PersonalDataNotifier ,当表单正确时,我们会将输入的数据传递给它。
这个屏幕上最重要的事情是监听每个字段的变化,并根据验证结果启用或禁用按钮。 我们将使用来自Form对象的回调onChange 。 在其中,我们将首先检查验证状态,然后将其传递给PersonalDataNotifier 。
形式( 键:_formKey, 自动验证:真, onChanged: () => _onFormChanged(personalDataNotifier), 孩子: 无效_onFormChanged(PersonalDataNotifierpersonalDataNotifier){ var isValid = _formKey.currentState.validate(); 个人数据通知器.onFormChanged(isValid); }
在PersonalDataNotifier中,我们将准备isFormValid变量。 我们将修改它(不要忘记调用notifyListeners() ),在视图中,我们将根据其值更改按钮状态。 请记住使用参数listen: true来获取Notifier实例——否则,我们的视图将不会重建,按钮状态将保持不变。
var personalDataNotifier = Provider.of<PersonalDataNotifier>(context, listen: true);
实际上,考虑到我们在其他地方使用了personalDataNotifier ,不需要重新加载视图,上面的行不是最佳的,应该将listen参数设置为false 。 我们唯一想要重新加载的是按钮,所以我们可以将它包装在一个经典的Consumer中:
消费者<PersonalDataNotifier>( builder: (context, notifier, child) { 返回凸起按钮( 孩子:文本(Strings.addressNext), onPressed:notifier.isFormValid ? /* 按钮启用时的动作 */ : 无效的, 颜色:颜色.蓝色, disabledColor:颜色.灰色, ); }, )
多亏了这一点,我们不会在每次使用通知程序时强制其他组件重新加载。
在显示个人数据的视图中,不会再有任何问题——我们可以访问PersonalDataNotifier并从那里下载更新的模型。
如何使用 BLoC 创建具有多个字段的表单
对于上一个屏幕,我们需要两个BLoC对象。 因此,当我们添加另一个“双屏”时,我们将总共有四个。 与Provider的情况一样,我们可以使用MultiBlocProvider来处理它,它的工作原理几乎相同。
无效 main() => runApp( MultiBlocProvider(提供者:[ 块提供者( 创建:(上下文)=> SocialMediaBloc(SimpleSocialMediaRepository()), ), 块提供者( 创建:(上下文)=> SocialMediaListBloc( mainBloc: BlocProvider.of<SocialMediaBloc>(context))), 块提供者( 创建:(上下文)=> PersonalDataBloc(), ), 块提供者( 创建:(上下文)=> PersonalDataInfoBloc( mainBloc: BlocProvider.of<PersonalDataBloc>(context)), ) ],孩子:ArchitecturesSampleApp()), );
与BLoC模式一样,最好从可能的状态和操作开始。
抽象类 PersonalDataState {} 类 NextButtonDisabled 扩展 PersonalDataState {} 类 NextButtonEnabled 扩展 PersonalDataState {} 类 InputFormCorrect 扩展 PersonalDataState { 最终的 PersonalData 模型; InputFormCorrect(this.model); }
此屏幕上发生的变化是按钮状态。 因此,我们需要单独的状态。 此外, InputFormCorrect状态将允许我们发送表单收集的数据。
抽象类 PersonalDataEvent {} 类 FormInputChanged 扩展 PersonalDataEvent { 最终布尔是有效的; FormInputChanged(this.isValid); } 类 FormCorrect 扩展 PersonalDataEvent { 最终的个人数据表格数据; FormCorrect(this.formData); }
监听表单的变化是至关重要的,因此FormInputChanged事件。 当表单正确时,将发送FormCorrect事件。
在验证方面,如果将其与 Provider 进行比较,这里会有很大的不同。 如果我们想将所有验证逻辑包含在BLoC层中,每个字段都会有很多事件。 此外,许多州会要求视图显示验证消息。
这当然是可能的,但这就像与TextFormField API 的斗争,而不是利用它的好处。 因此,如果没有明确的原因,您可以将验证留在视图层中。
按钮状态将取决于BLoC发送到视图的状态:
BlocBuilder<PersonalDataBloc, PersonalDataState>( 构建器:(上下文,状态){ 返回凸起按钮( 孩子:文本(Strings.addressNext), onPressed:状态为 NextButtonEnabled ? /* 按钮启用时的动作 */ : 无效的, 颜色:颜色.蓝色, disabledColor:颜色.灰色, ); })
PersonalDataBloc中的事件处理和状态映射如下:
@覆盖 Stream<PersonalDataState> mapEventToState(PersonalDataEvent 事件) async* { 如果(事件是FormCorrect){ 产量 InputFormCorrect(event.formData); } else if (event is FormInputChanged) { 产量 mapFormInputChangedToState(event); } } PersonalDataState mapFormInputChangedToState(FormInputChanged 事件) { 如果(事件.isValid){ 返回 NextButtonEnabled(); } 别的 { 返回 NextButtonDisabled(); } }
至于带有个人数据摘要的屏幕,情况与前面的示例类似。 附加到此屏幕的BLoC将从表单屏幕的BLoC中检索模型信息。
类 PersonalDataInfoBloc 扩展 Bloc<PersonalDataInfoEvent, PersonalDataInfoState> { 最终的 PersonalDataBloc 主块; PersonalDataInfoBloc({@required this.mainBloc}) { mainBloc.listen((状态){ 如果(状态为 InputFormCorrect){ 添加(PersonalDataInfoScreenStart(state.model)); } }); } @覆盖 PersonalDataInfoState 获取初始状态 => InfoEmpty(); @覆盖 Stream<PersonalDataInfoState> mapEventToState(PersonalDataInfoEvent 事件) async* { 如果(事件是 PersonalDataInfoScreenStart){ 产量 InfoLoaded(event.model); } } }
Flutter 架构:要记住的注意事项
上面的例子足以表明两种架构之间存在明显的差异。 BLoC 很好地将视图层与业务逻辑分离。 这需要更好的可重用性和可测试性。 似乎要处理简单的情况,您需要编写比Provider更多的代码。 如您所知,在这种情况下,随着应用程序复杂性的增加,这种 Flutter 架构将变得更加有用。
想为您的企业构建面向未来的应用程序吗?
让我们取得联系Provider还将 UI 与逻辑很好地分离,不会强制为每个用户交互创建单独的状态,这意味着您通常不必编写大量代码来处理简单的案例。 但这可能会在更复杂的情况下导致问题。
点击这里查看整个项目。