作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
马尔科·佩鲁托维奇的头像

Marko Perutović

Marko拥有超过13年的不同技术和团队领导经验. 在编码时,他喜欢“保持简短和简单”.”

Years of Experience

20

Share

去年年中,我想将一款Android应用移植到iOS和网页上. Flutter 是手机平台的选择,我在考虑网页端应该选择什么.

而我却对Flutter一见钟情, 我仍然有一些保留意见:当沿着小部件树传播状态时, Flutter’s InheritedWidget 或redux -及其所有变体-将完成这项工作,但需要一个新的 类似Flutter的框架你会期望视图层会更活跃一些.e., 小部件本身是无状态的, 并根据外界喂养的状态变化, but they aren’t. Also, Flutter 只支持Android和iOS,但我想发布到网页上. 我的应用程序中已经有了大量的业务逻辑,我希望尽可能地重用它们, 而且,为了业务逻辑中的单个更改而在至少两个地方更改代码的想法是不可接受的.

我开始四处寻找如何克服这个问题,然后遇到了BLoC. 作为一个简短的介绍,我建议大家观看 Flutter/AngularDart -代码共享,更好地结合在一起(DartConf 2018) 等你有时间的时候.

BLoC Pattern

视图层、块层、存储库层和数据层中的通信流关系图

BLoC是谷歌发明的一个花哨的词,意思是“business logic components.“BLoC模式的思想是将尽可能多的业务逻辑存储在纯Dart代码中,以便其他平台可以重用它. 要做到这一点,你必须遵循以下规则:

  • 分层沟通. 视图与BLoC层通信, 哪个与存储库通信, 存储库与数据层对话. 沟通时不要跳过几层.
  • 通过接口通信. 接口必须用纯粹的、独立于平台的Dart代码编写. 有关更多信息,请参见 隐式接口.
  • 块只暴露流和汇. BLoC的I/O将在后面讨论.
  • Keep views simple. 将业务逻辑排除在视图之外. 它们应该只显示数据并响应用户交互.
  • 使BLoCs与平台无关. block是纯Dart代码,因此它们不应该包含特定于平台的逻辑或依赖项. 不要分支到平台条件代码. block是在纯Dart中实现的逻辑,并且在处理基本平台之上.
  • 注入特定于平台的依赖. 这听起来可能与上面的规则相矛盾,但请听我说完. block本身是平台无关的, 但是,如果它们需要与特定于平台的存储库进行通信呢? Inject it. 通过确保通过接口进行通信并注入这些存储库, 我们可以肯定的是,无论你的存储库是为Flutter还是AngularDart编写的, 欧盟不会在意的.

最后要记住的一点是,BLoC的输入应该是a sink,而输出则通过a stream. 它们都是 StreamController.

如果你在写网页(或手机)时严格遵守这些规则!)应用程序,创建一个移动(或web!)版本可以像制作视图和平台特定接口一样简单. 即使你刚刚开始使用AngularDart或Flutter, 使用基本的平台知识制作视图仍然很容易. 您可能最终会重用超过一半的代码库. BLoC模式使一切都保持结构化和易于维护.

构建一个AngularDart和Flutter BLoC Todo应用程序

I made a simple todo app 在Flutter和AngularDart. 该应用程序使用Firecloud作为后端和响应式方法来创建视图. 该应用程序由三个部分组成:

  • bloc
  • todo_app_flutter
  • todoapp_dart_angular

您可以选择拥有更多的部件—例如,数据接口、本地化接口等. 需要记住的是,每一层都应该通过接口与另一层进行通信.

The BLoC Code

In the bloc/ directory:

  • lib/src/bloc: BloC模块作为包含业务逻辑的纯Dart库存储在这里.
  • lib/src/repository:数据接口存储在该目录下.
  • lib / src /仓库/ firestore:存储库包含FireCloud的数据接口及其模型, 由于这是一个示例应用, 我们只有一个数据模型 todo.dart 还有一个数据接口 todo_repository.dart; however, in a real-world app, there will be more models and repository interfaces.
  • lib / src /仓库/偏好 contains preferences_interface.dart, 一个简单的界面,可以将成功登录的用户名存储到web上的本地存储或移动设备上的共享偏好.
//BLOC
抽象类PreferencesInterface{
//Preferences
 最终DEFAULT_USERNAME = "DEFAULT_USERNAME";

 未来initPreferences ();
 获取defaultUsername;
 无效setDefaultUsername(字符串用户名);
}

Web和移动实现必须将此实现到商店,并从本地存储/首选项获取默认用户名. AngularDart的实现是这样的:

// ANGULAR DART
类PreferencesInterfaceImpl扩展PreferencesInterface {

 SharedPreferences _prefs;

 @override
 Future initPreferences() async => _prefs = await SharedPreferences.getInstance();

 @override
 void setDefaultUsername(String username) => _prefs.setString (DEFAULT_USERNAME、用户名);
 @override
 String get defaultUsername => _prefs.getString (DEFAULT_USERNAME);
}

这里没有什么特别的——它实现了它需要的东西. 你可能会注意到 initPreferences() 返回的异步方法 null. 该方法需要在颤振端实现,因为得到 SharedPreferences 手机上的实例是异步的.

//FLUTTER
@override
Future initPreferences() async => _prefs = await SharedPreferences.getInstance();

让我们稍微关注一下lib/src/bloc目录. 任何处理某些业务逻辑的视图都应该有它的BLoC组件. 在这个目录中,您将看到视图块 base_bloc.dart, endpoints.dart, and session.dart. 最后一个负责为用户签入和签出,并为存储库接口提供端点. 会话接口存在的原因是 firebase and firecloud web和手机的包是不一样的,必须基于平台来实现.

// BLOC
抽象类Session实现端点{

 //Collections.
 @protected
 userCollectionName = "users";
 @protected
 todoCollectionName = "todos";
 String userId;

 Session(){
   _isSignedIn.stream.听((signedIn) {
     if(!signedIn) _logout ();
   });
 }

 final BehaviorSubject _isSignedIn = BehaviorSubject();
 Stream get isSignedIn => _isSignedIn.stream;
 Sink get signedIn => _isSignedIn.sink;

 Future signIn(String username, String password);
 @protected
 void logout();

 void _logout() {
   logout();
   userId = null;
 }
}

其思想是使会话类保持全局(单例)。. Based on its _isSignedIn.stream getter, 它处理应用程序在登录/待办事项列表视图之间的切换,并在userId存在的情况下为存储库实现提供端点.e.,用户已登录).

base_bloc.dart 是所有集团的基础. 在本例中,它根据需要处理加载指示符和错误对话框显示.

对于业务逻辑示例,我们将看一下 todo_add_edit_bloc.dart. 该文件的长名称解释了它的目的. 它有一个私有的void方法 _addUpdateTodo (bool addUpdate).

// BLOC
void _addUpdateTodo (bool addUpdate) {
 if(!addUpdate) return;
 //Check required.
 if(_title.value.isEmpty)
   _todoError.sink.add(0);
 else if (_description.value.isEmpty)
   _todoError.sink.add(1);
 else
   _todoError.sink.add(-1);

 if(_todoError.value >= 0)
   return;

 最后TodoBloc TodoBloc = _todo.value == null ? TodoBloc("", false, DateTime.Now (), null, null, null): _todo.value;
 todoBloc.title = _title.value;
 todoBloc.Description = _description.value;

 showProgress.add(true);
 _toDoRepository.addUpdateToDo (todoBloc)
     .doOnDone( () => showProgress.add(false) )
     .listen((_) => _closeDetail.add(true) ,
     onError: (err) => error.add( err.toString()) );
}

这个方法的输入是 bool addUpdate 它是一个听众 final BehaviorSubject _addUpdate = BehaviorSubject(). 当用户点击应用中的保存按钮时, 该事件发送该主题sink true值并触发该BLoC函数. 这段flutter代码在视图侧发挥了神奇的作用.

// FLUTTER
IconButton(图标:图标(图标.done), onPressed: () => _todoAddEditBloc.addUpdateSink.add(true),),

` _addUpdateTodo 检查标题和描述是否为空,并更改的值 _todoError 基于此条件的行为主题. The _todoError 如果没有提供任何值,Error负责在输入字段上触发视图错误显示. 如果一切正常,则检查是否创建或更新 TodoBloc and finally _toDoRepository '写入FireCloud.

业务逻辑在这里,但请注意:

  • 在BLoC中,只有流和汇是公开的. _addUpdateTodo 是私有的,不能从视图访问.
  • _title.value and _description.value 是由用户在文本输入中输入值填充的吗. 文本更改事件的文本输入将其值发送到相应的接收器. 这样,我们就可以对BLoC中的值进行响应式更改,并在视图中显示这些值.
  • _toDoRepository 是否依赖于平台并由注入提供.

的代码 todo_list.dart BLoC _getTodos() method. 它监听todo集合的快照,并将集合数据流式传输到其视图中的列表. 视图列表将根据集合流更改重新绘制.

// BLOC
void _getTodos(){
 showProgress.add(true);
 _toDoRepository.getToDos()
     .听((todosList) {
       todosSink.add(todosList);
       showProgress.add(false);
       },
     onError: (err) {
       showProgress.add(false);
       error.add(err.toString());
     });
}

在使用流或rx等价物时,需要注意的重要事项是流必须是关闭的. We do that in the dispose() 各集团的方法. 在其Dispose /destroy方法中处置每个视图的BLoC.

// FLUTTER

@override
void dispose() {
 widget.baseBloc.dispose();
 super.dispose();
}

或者在AngularDart项目中:


// ANGULAR DART
@override
void ngOnDestroy() {
 todoListBloc.dispose();
}

注入特定于平台的存储库

BLoC模式、todo存储库等之间的关系图

我们之前说过,BLoC中的所有东西都必须是简单的Dart,不依赖于平台. TodoAddEditBloc needs ToDoRepository 给Firestore写信. Firebase有平台相关的包,我们必须有单独的实现 ToDoRepository interface. 这些实现被注入到应用程序中. 对于Flutter,我使用了 flutter_simple_dependency_injection 包,它看起来像这样:


// FLUTTER
class Injection {

 static Firestore _firestore = Firestore.instance;
 static FirebaseAuth _auth = FirebaseAuth.instance;
 static PreferencesInterface _preferencesInterface = PreferencesInterfaceImpl();

 静态喷射器;
 未来initInjection() async {
   等待_preferencesInterface.initPreferences();
   注入器.getInjector();
   //Session
   injector.map((i) => SessionImpl(_auth, _firestore), isSingleton: true);
   //Repository
   injector.map((i) => ToDoRepositoryImpl(injector.get()), isSingleton: false);
   //Bloc
   injector.map((i) => LoginBloc(_preferencesInterface, injector.get()), isSingleton: false);
   injector.map((i) => TodoListBloc(injector.get(), injector.get()), isSingleton: false);
   injector.map((i) => TodoAddEditBloc(injector.get()), isSingleton: false);
 }
}

在这样的小部件中使用它:

// FLUTTER
TodoAddEditBloc _todoAddEditBloc =注入.injector.get();

AngularDart通过providers内置了注入.

// ANGULAR DART
@GenerateInjector([
 ClassProvider(PreferencesInterface, useClass: PreferencesInterfaceImpl),
 ClassProvider(Session, useClass: SessionImpl),
 ExistingProvider(端点,会话)
])

在一个组件中:

// ANGULAR DART
providers: [
 overlayBindings,
 ClassProvider(ToDoRepository, useClass: ToDoRepositoryImpl),
 ClassProvider (TodoAddEditBloc),
 ExistingProvider (BaseBloc TodoAddEditBloc)
],

We can see that Session is global. 它提供了中使用的登录/注销功能和端点 ToDoRepository and BLoCs. ToDoRepository 中实现的端点接口 SessionImpl and so on. 视野只能看到它的阵营,别无其他.

Views

在BLoC和视图之间交互的汇流图

视图应该尽可能简单. 它们只显示来自BLoC的内容,并将用户的输入发送到BLoC. 我们来复习一下 TodoAddEdit widget从Flutter和它的web等效 TodoDetailComponent. 它们显示所选的待办事项标题和描述,用户可以添加或更新待办事项.

Flutter:

// FLUTTER
_todoAddEditBloc.todoStream.first.then((todo) {
 _titleController.text = todo.title;
 _descriptionController.text = todo.description;
});

And later in code…

// FLUTTER
StreamBuilder(
 流:_todoAddEditBloc.todoErrorStream,
 (BuildContext context, AsyncSnapshot errorSnapshot) {
   return TextField(
     onChanged: (text) => _todoAddEditBloc.titleSink.add(text),
     InputDecoration(hintttext:本地化.of(context).title, labelText:本地化.of(context).title, errorText: errorSnapshot.data == 0 ? Localization.of(context).titleEmpty : null),
     控制器:_titleController,
   );
 },
),

The StreamBuilder 如果出现错误(没有插入任何内容),小部件会自行重建。. 这是通过倾听来实现的 _todoAddEditBloc.todoErrorStream . _todoAddEditBloc.titleSink , 哪个是BLoC中的接收器,它保存标题,并在用户在文本字段中输入文本时更新.

这个输入字段的初始值(如果选择了一个todo)通过侦听来填充 _todoAddEditBloc.todoStream 如果我们添加一个新的待办事项,哪个会保存选中的待办事项,哪个会保存空的待办事项.

给文本字段赋值是由它的控制器完成的 _titleController.text = todo.title; .

当用户决定保存待办事项时,它按下应用程序栏中的复选图标并触发 _todoAddEditBloc.addUpdateSink.add(true). That invokes the _addUpdateTodo (bool addUpdate) 并完成添加的所有业务逻辑, updating, 或者将错误显示给用户.

一切都是响应式的,不需要处理小部件状态.

AngularDart的代码甚至更简单. 将组件提供给它的BLoC之后,使用提供者 todo_detail.html 文件代码负责显示数据并将用户交互发送回BLoC.

// AngularDart





   {{saveStr}}



和Flutter一样,我们正在分配 ngModel= 来自标题流的值,这是它的初始值.

// AngularDart
(inputKeyPress) = " todoAddEditBloc.descriptionSink.add($event)"

The inputKeyPress output事件将用户在文本输入中键入的字符发送回BLoC的描述. 材质按钮 = " todoAddEditBloc(触发).addUpdateSink.add(true)" 事件发送BLoC添加/更新事件,再次触发相同的事件 _addUpdateTodo (bool addUpdate) 在集团内的职能. 如果你看一下 todo_detail.dart 组件代码, 您将看到,除了显示在视图上的字符串之外,几乎什么都没有. 我把它们放在那里,而不是在HTML中,因为可能的本地化可以在这里完成.

其他组件也是如此——组件和小部件没有业务逻辑.

还有一个场景值得一提. 假设您有一个具有复杂数据表示逻辑的视图,或者具有必须格式化值(日期)的表, currencies, etc.). 有人可能会试图从BLoC获取值并在视图中格式化它们. That’s wrong! 视图中显示的值应该到达已经格式化的视图(字符串). 原因是格式本身也是业务逻辑. 另一个例子是,显示值的格式取决于一些可以在运行时更改的应用程序参数. 通过向BLoC提供该参数并使用响应式方法来查看显示, 业务逻辑将格式化值并只重新绘制所需的部分. 这个例子中的BLoC模型, TodoBloc, is very simple. 从FireCloud模型到BLoC模型的转换是在存储库中完成的, but if needed, 这可以在BLoC中完成,这样模型值就可以显示了.

Wrapping Up

这篇简短的文章涵盖了BLoC模式实现的主要概念. 这证明了Flutter和AngularDart之间的代码共享是可能的, 允许本地, and 跨平台开发.

探索示例, you will see that, 当实现正确时, BLoC大大缩短了开发手机/网页应用的时间. An example is ToDoRepository 以及它的实现. 实现代码几乎是相同的,甚至视图组合逻辑也是相似的. 在完成几个小部件/组件后,您可以快速开始批量生产.

我希望这篇文章能让你看到我制作网页/移动应用的乐趣和热情 Flutter 以及AngularDart和BLoC模式. 如果您想用JavaScript构建跨平台桌面应用程序,请阅读本文 电子:跨平台桌面应用变得简单 由托普塔勒同事施塔芬·P. Péricat.

了解基本知识

  • What is Flutter?

    Flutter是一个移动(Android/iOS)开发平台. 它专注于本地高质量的用户体验和快速开发丰富的UI应用程序.

  • Flutter用的是什么语言?

    Flutter使用Dart,一种由Google开发的现代、简洁、面向对象的语言.

  • 什么是AngularDart?

    AngularDart是Angular到Dart的一个移植. 它的Dart代码被编译成JavaScript.

  • AngularDart支持哪些浏览器?

    编译器支持IE11、Chrome、Edge、Firefox和Safari.

  • 什么是BLoC模式?

    “业务逻辑组件”或BLoC是一种开发模式. BLoC的理念是在纯Dart代码中隔离尽可能多的业务逻辑. 这提供了一个可以在移动和web平台之间共享的代码库.

  • 使用BLoC图案的规则是什么?

    为BLoC构建的应用程序必须分层. 视图层、业务逻辑层、数据访问层等. must be separated. 每一层都通过接口与下一层通信. 接口必须是纯Dart代码,接口的实现可以是特定于平台的(Flutter或AngularDart)。.

  • BLoC组件的规则是什么?

    block的输入和输出只能是流和汇, 依赖必须是可注入的和平台无关的, 业务逻辑不应该有平台分支, 你可以处理视图状态或使用响应式方法.

  • 在UI方面,BLoC是否采用了响应式方法?

    BLoC模式不关心视图以及它如何处理用户显示/交互. But, 因为它只使用流和汇作为输出和输入, 它完美地为视图侧的响应式方法量身定制.

  • Flutter vs. 其他移动建筑选择

    Flutter与大多数其他解决方案的不同之处在于,它使用自己的高性能渲染引擎来绘制小部件.

聘请Toptal这方面的专家.
Hire Now
马尔科·佩鲁托维奇的头像
Marko Perutović

Located in Split, Croatia

Member since January 14, 2015

About the author

Marko拥有超过13年的不同技术和团队领导经验. 在编码时,他喜欢“保持简短和简单”.”

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Years of Experience

20

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.