个性化阅读
专注于IT技术分析

如何在Flutter和AngularDart中利用BLoC进行代码共享

本文概述

去年中旬, 我想将一个Android应用程序移植到iOS和Web上。 Flutter是移动平台的选择, 我正在考虑为Web端选择什么。

当我一见钟情Flutter时, 我仍然有一些保留:在小部件树中传播状态时, Flutter的InheritedWidget或Redux及其所有变体都可以完成工作, 但是有了Flutter这样的新框架, 你将期望视图层会更具反应性, 即小部件本身将是无状态的, 并根据从外部馈送的状态进行更改, 但实际上并非如此。 Aslo, Flutter仅支持Android和iOS, 但我想发布到网络上。我的应用程序中已经有大量的业务逻辑, 并且我想尽可能地重用它, 并且一次更改业务逻辑至少要在两个位置更改代码的想法是不可接受的。

我开始研究如何解决这个问题, 并遇到了BLoC。为了快速介绍一下, 我建议你在有空的时候观看Flutter / AngularDart –代码共享, 更好地一起使用(DartConf 2018)。

BLoC模式

视图,BLoC,存储库和数据层中的通信流图

BLoC是Google发明的一个花哨的词, 意为”业务逻辑组件”。 BLoC模式的想法是将尽可能多的业务逻辑存储在纯Dart代码中, 以便可以被其他平台重用。为此, 必须遵循一些规则:

  • 分层沟通。视图与BLoC层进行通信, 后者与存储库进行通信, 存储库与数据层进行通信。交流时不要跳过图层。
  • 通过接口进行通信。接口必须使用与平台无关的纯Dart代码编写。有关更多信息, 请参见隐式接口的文档。
  • BLoC仅公开流和接收器。 BLoC的I / O将在后面讨论。
  • 保持视图简单。将业务逻辑放在视图之外。他们应该只显示数据并响应用户交互。
  • 使BLoCs平台不可知。 BLoC是纯Dart代码, 因此它们不应包含平台特定的逻辑或依赖项。不要进入平台条件代码。 BLoC是在纯Dart中实现的逻辑, 并且在上面处理基本平台。
  • 注入特定于平台的依赖项。这听起来与上述规则矛盾, 但请听我说。 BLoC本身与平台无关, 但是如果它们需要与特定于平台的存储库进行通信怎么办?注入它。通过确保通过接口进行通信并注入这些存储库, 我们可以确保无论你的存储库是为Flutter还是AngularDart编写的, BLoC都不会在意。

要记住的最后一件事是, BLoC的输入应该是接收器, 而输出是通过流的。这些都是StreamController的一部分。

如果你在编写Web(或Web!)应用程序时严格遵守这些规则, 则创建移动(或Web!)版本就像创建视图和特定于平台的界面一样简单。即使你刚刚开始使用AngularDart或Flutter, 使用基础平台知识进行视图仍然很容易。你可能最终会重用一半以上的代码库。 BLoC模式使所有内容保持结构化并易于维护。

构建AngularDart和Flutter BLoC Todo应用程序

我在Flutter和AngularDart中制作了一个简单的待办事项应用程序。该应用程序使用Firecloud作为后端和一种被动的方法来创建视图。该应用程序包含三个部分:

  • todo_app_flutter
  • todoapp_dart_angular

你可以选择包含更多部分, 例如数据接口, 本地化接口等。需要记住的是, 每一层都应该通过接口与另一层进行通信。

BLoC代码

在bloc /目录中:

  • lib / src / bloc:BloC模块在此处存储为纯Dart库, 其中包含业务逻辑。
  • lib / src / repository:数据接口存储在目录中。
  • lib / src / repository / firestore:存储库包含用于数据的FireCloud接口及其模型, 由于这是一个示例应用程序, 因此我们只有一个数据模型todo.dart和一个数据todo_repository.dart接口;但是, 在实际应用中, 将有更多的模型和存储库界面。
  • lib / src / repository / preferences包含preferences_interface.dart, 这是一个简单的界面, 可将登录的用户名成功存储到Web上的本地存储或移动设备上的共享首选项中。
//BLOC
abstract class PreferencesInterface{
//Preferences
 final DEFAULT_USERNAME = "DEFAULT_USERNAME";

 Future initPreferences();
 String get defaultUsername;
 void setDefaultUsername(String username);
}

Web和移动实现必须将其实现到商店, 并从本地存储/首选项中获取默认用户名。的AngularDart实现如下所示:

// ANGULAR DART
class PreferencesInterfaceImpl extends PreferencesInterface {

 SharedPreferences _prefs;

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

 @override
 void setDefaultUsername(String username) => _prefs.setString(DEFAULT_USERNAME, username);
 @override
 String get defaultUsername => _prefs.getString(DEFAULT_USERNAME);
}

这里没什么壮观的—它可以实现所需的功能。你可能会注意到initPreferences()异步方法返回null。由于在移动设备上获取SharedPreferences实例是异步的, 因此需要在Flutter端实现此方法。

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

让我们继续介绍lib / src / bloc目录。处理某些业务逻辑的任何视图都应具有其BLoC组件。在此目录中, 你将看到BLOC的base_bloc.dart, endpoints.dart和session.dart。最后一个负责登录和注销用户, 并为存储库接口提供端点。会话界面存在的原因是, firebase和firecloud软件包对于Web和移动设备而言是不同的, 必须基于平台实施。

// BLOC
abstract class Session implements Endpoints {

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

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

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

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

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

这个想法是使会话类保持全局(单个)。基于_isSignedIn.stream getter, 它处理登录/待办事项列表视图之间的应用切换, 并在存在userId(即用户已登录)的情况下向存储库实现提供端点。

base_bloc.dart是所有BLoC的基础。在此示例中, 它根据需要处理负载指示符和错误对话框显示。

对于业务逻辑示例, 我们将看看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;

 final 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, 它是最终BehaviorSubject <bool> _addUpdate = BehaviorSubject <bool>()的侦听器。当用户单击应用程序中的”保存”按钮时, 事件将发送此主题接收器真实值并触发此BLoC函数。这段颤动的代码在视图方面起到了神奇的作用。

// FLUTTER
IconButton(icon: Icon(Icons.done), onPressed: () => _todoAddEditBloc.addUpdateSink.add(true), ), 

_addUpdateTodo检查标题和描述是否都不为空, 并根据此条件更改_todoError BehaviorSubject的值。如果未提供任何值, 则_todoError错误负责触发输入字段上的视图错误显示。如果一切正常, 它将检查是否创建或更新TodoBloc, 最后_toDoRepository将写入FireCloud。

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

  • 在BLoC中仅公开流和接收器。 _addUpdateTodo是私有的, 无法从视图访问。
  • _title.value和_description.value由用户在文本输入中输入值来填充。文本更改事件上的文本输入将其值发送到相应的接收器。这样, 我们可以在BLoC中对值进行反应性更改, 并在视图中显示它们。
  • _toDoRepository依赖于平台, 并通过注入提供。

查看todo_list.dart BLoC _getTodos()方法的代码。它侦听todo集合的快照, 并将集合数据流式传输到其视图中列出。视图列表根据收集流的更改重绘。

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

使用流或等效rx时要意识到的重要一点是必须关闭流。我们在每个BLoC的dispose()方法中执行此操作。使用其处置/销毁方法处置每个视图的BLoC。

// FLUTTER

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

或在AngularDart项目中:

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

注入特定于平台的存储库

BLoC模式,待办事项存储库等之间的关系图

我们之前说过, BLoC中包含的所有内容都必须是简单的Dart, 并且不依赖平台。 TodoAddEditBloc需要ToDoRepository才能写入Firestore。 Firebase具有依赖于平台的软件包, 我们必须具有ToDoRepository接口的单独实现。这些实现被注入到应用程序中。对于Flutter, 我使用了flutter_simple_dependency_injection包, 它看起来像这样:

// FLUTTER
class Injection {

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

 static Injector injector;
 static Future initInjection() async {
   await _preferencesInterface.initPreferences();
   injector = Injector.getInjector();
   //Session
   injector.map<Session>((i) => SessionImpl(_auth, _firestore), isSingleton: true);
   //Repository
   injector.map<ToDoRepository>((i) => ToDoRepositoryImpl(injector.get<Session>()), isSingleton: false);
   //Bloc
   injector.map<LoginBloc>((i) => LoginBloc(_preferencesInterface, injector.get<Session>()), isSingleton: false);
   injector.map<TodoListBloc>((i) => TodoListBloc(injector.get<ToDoRepository>(), injector.get<Session>()), isSingleton: false);
   injector.map<TodoAddEditBloc>((i) => TodoAddEditBloc(injector.get<ToDoRepository>()), isSingleton: false);
 }
}

在这样的小部件中使用此命令:

// FLUTTER
TodoAddEditBloc _todoAddEditBloc = Injection.injector.get<TodoAddEditBloc>();

AngularDart通过提供程序内置了注入功能。

// ANGULAR DART
@GenerateInjector([
 ClassProvider(PreferencesInterface, useClass: PreferencesInterfaceImpl), ClassProvider(Session, useClass: SessionImpl), ExistingProvider(Endpoints, Session)
])

在组件中:

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

我们可以看到Session是全球性的。它提供了ToDoRepository和BLoC中使用的登录/注销功能和端点。 ToDoRepository需要使用SessionImpl等实现的终结点接口。该视图应仅看到其BLoC, 仅此而已。

观看次数

BLoC和视图之间相互作用的汇和流图

视图应尽可能简单。它们仅显示来自BLoC的内容, 并将用户的输入发送到BLoC。我们将使用Flutter的TodoAddEdit小部件及其与网络等效的TodoDetailComponent进行介绍。它们显示选定的待办事项标题和说明, 用户可以添加或更新待办事项。

扑:

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

然后在代码中…

// FLUTTER
StreamBuilder<int>(
 stream: _todoAddEditBloc.todoErrorStream, builder: (BuildContext context, AsyncSnapshot errorSnapshot) {
   return TextField(
     onChanged: (text) => _todoAddEditBloc.titleSink.add(text), decoration: InputDecoration(hintText: Localization.of(context).title, labelText: Localization.of(context).title, errorText: errorSnapshot.data == 0 ? Localization.of(context).titleEmpty : null), controller: _titleController, );
 }, ), 

如果发生错误(未插入任何内容), 则StreamBuilder小部件将自行重建。这是通过听_todoAddEditBloc.todoErrorStream发生的。 _todoAddEditBloc.titleSink, 它是BLoC中的一个接收器, 用于保存标题, 并且在用户在文本字段中输入文本时进行更新。

通过监听_todoAddEditBloc.todoStream来填充此输入字段的初始值(如果选择了一个待办事项), 该值保存了所选的待办事项;如果添加新的待办事项, 则为空。

通过其控制器_titleController.text = todo.title;将值分配给文本字段。 。

当用户决定保存待办事项时, 它将按应用程序栏中的选中图标, 并触发_todoAddEditBloc.addUpdateSink.add(true)。这将调用我们在上一个BLoC部分中讨论的_addUpdateTodo(bool addUpdate), 并执行添加, 更新或向用户显示错误的所有业务逻辑。

一切都是反应性的, 不需要处理小部件状态。

AngularDart代码甚至更简单。在使用提供程序为组件提供其BLoC之后, todo_detail.html文件代码完成了显示数据并将用户交互发送回BLoC的部分。

// AngularDart
<material-input
       #title
       label="{{titleStr}}"
       ngModel="{{(todoAddEditBloc.titleStream | async) == null ? '' : (todoAddEditBloc.titleStream | async)}}"
       (inputKeyPress)="todoAddEditBloc.titleSink.add($event)"
       [error]="(todoAddEditBloc.todoErrorStream | async) == 0 ? titleErrString : ''"
       autoFocus floatingLabel style="width:100%"
       type="text"
       useNativeValidation="false"
       autocomplete="off">
</material-input>
<material-input
       #description
       label="{{descriptionStr}}"
       ngModel="{{(todoAddEditBloc.descriptionStream | async) == null ? '' : (todoAddEditBloc.descriptionStream | async)}}"
       (inputKeyPress)="todoAddEditBloc.descriptionSink.add($event)"
       [error]="(todoAddEditBloc.todoErrorStream | async) == 1 ? descriptionErrString : ''"
       autoFocus floatingLabel style="width:100%"
       type="text"
       useNativeValidation="false"
       autocomplete="off">
</material-input>
<material-button
       animated
       raised
       role="button"
       class="blue"
       (trigger)="todoAddEditBloc.addUpdateSink.add(true)">
   {{saveStr}}
</material-button>

<base-bloc></base-bloc>

与Flutter类似, 我们从标题流中为其分配ngModel =值, 这是其初始值。

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

inputKeyPress输出事件会将用户在文本输入中键入的字符发送回BLoC的描述。物料按钮(trigger)=” todoAddEditBloc.addUpdateSink.add(true)”事件发送BLoC添加/更新事件, 该事件再次触发BLoC中相同的_addUpdateTodo(bool addUpdate)函数。如果查看该组件的todo_detail.dart代码, 你将看到除了视图上显示的字符串之外几乎没有任何内容。我将它们放在此处而不是HTML中, 因为可以在此处进行本地化。

其他所有组件也是如此—组件和小部件的业务逻辑为零。

另一种情况值得一提。想象一下, 你有一个具有复杂数据表示逻辑的视图, 或者像是一个表, 其值必须被格式化(日期, 货币等)。可能有人会从BLoC获取值并将其格式化为视图。错了!在视图中显示的值应该出现在已经格式化的视图中(字符串)。这样做的原因是格式化本身也是业务逻辑。另一个示例是, 显示值的格式取决于某些可在运行时更改的应用程序参数。通过将该参数提供给BLoC并使用反应性方法来查看显示, 业务逻辑将格式化该值并仅重绘所需的零件。在此示例中, 我们拥有的BLoC模型TodoBloc非常简单。从FireCloud模型到BLoC模型的转换是在存储库中完成的, 但是如果需要, 可以在BLoC中进行转换, 以便模型值可供显示。

本文总结

本文简要介绍了BLoC模式实现的主要概念。事实证明, Flutter和AngularDart之间可以共享代码, 从而可以进行本机跨平台开发。

在该示例中, 你会发现, 如果实施正确, BLoC会大大缩短创建移动/ Web应用程序的时间。一个示例是ToDoRepository及其实现。实现代码几乎相同, 甚至视图组成逻辑也相似。经过几个小部件/组件, 你可以快速开始批量生产。

我希望本文能使你对我使用Flutter / AngularDart和BLoC模式制作Web /移动应用程序的乐趣和热情有所了解。如果你希望使用JavaScript构建跨平台的桌面应用程序, 请阅读srcminierStéphaneP.Péricat的同伴Electron:跨平台的桌面应用程序。

相关文章:Dart语言:Java和C#不够清晰时

赞(1)
未经允许不得转载:srcmini » 如何在Flutter和AngularDart中利用BLoC进行代码共享

评论 抢沙发

评论前必须登录!