博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[翻译] 响应式编程(Reactive Programming) - 流(Streams) - BLoC - 实际应用案例
阅读量:6257 次
发布时间:2019-06-22

本文共 41000 字,大约阅读时间需要 136 分钟。

非常感谢 同意我将它的一些文章翻译为中文发表,这是其中一篇。

本文应用多个实例详细讲解了 BLoC 设计模式的原理和使用方法,非常值得学习。

原文

特别提醒:本文很长,阅读完需要较长时间

 

BLoC 设计模式、响应式编程、流、应用案例、和有用的模式。

难度:中等

简介

前一段时间,我介绍了 BLoC响应式编程(Reactive Programming )流(Streams) 的概念后,我想给大家分享一些我经常使用并且非常有用的(至少对我而言)模式应该是一件有趣的事。

我本文要讲的主题有这些:

  • 允许根据事件响应状态的改变

  • 允许根据输入和验证规则控制表单的行为 我的例子还包括密码和重新输入密码的比较。

  • 允许一个 Widget 根据其是否存在某一个列表中来调整其行为。

本文完整的代码在 上可以获取到。

1. BLoC Provider 和 InheritedWidget

我借此文章的机会介绍我另一个版本的 BlocProvider,它现在依赖一个 InheritedWidget

使用 InheritedWidget 的好处是我们可以提高 APP 的 性能

请容我细细道来……

1.1. 之前的实现方式

我之前版本的 BlocProvider 实现为一个常规 StatefulWidget,如下所示:

abstract class BlocBase {  void dispose();}// Generic BLoC providerclass BlocProvider
extends StatefulWidget { BlocProvider({ Key key, @required this.child, @required this.bloc, }): super(key: key); final T bloc; final Widget child; @override _BlocProviderState
createState() => _BlocProviderState
(); static T of
(BuildContext context){ final type = _typeOf
>(); BlocProvider
provider = context.ancestorWidgetOfExactType(type); return provider.bloc; } static Type _typeOf
() => T;}class _BlocProviderState
extends State
>{ @override void dispose(){ widget.bloc.dispose(); super.dispose(); } @override Widget build(BuildContext context){ return widget.child; }}复制代码

我使用 StatefulWidget 利用其 dispose() 方法以确保在不再需要时释放 BLoC 分配的资源。

这很好用,但从性能角度来看并不是最佳的。

context.ancestorWidgetOfExactType() 是一个 O(n) 复杂度的方法。为了获取需要的某种特定类型的祖先,它从上下文开始向上遍历树,一次递归地向上移动一个父节点,直到完成。如果从当前上下文到祖先的距离很小,则调用此函数还是可以接受的,否则应该避免调用此函数。 这是这个函数的代码。

@overrideWidget ancestorWidgetOfExactType(Type targetType) {    assert(_debugCheckStateIsActiveForAncestorLookup());    Element ancestor = _parent;    while (ancestor != null && ancestor.widget.runtimeType != targetType)        ancestor = ancestor._parent;    return ancestor?.widget;}复制代码

1.2. 新的实现方式

新的实现方式依赖于 StatefulWidget,并结合了 InheritedWidget

Type _typeOf
() => T;abstract class BlocBase { void dispose();}class BlocProvider
extends StatefulWidget { BlocProvider({ Key key, @required this.child, @required this.bloc, }): super(key: key); final Widget child; final T bloc; @override _BlocProviderState
createState() => _BlocProviderState
(); static T of
(BuildContext context){ final type = _typeOf<_BlocProviderInherited
>(); _BlocProviderInherited
provider = context.ancestorInheritedElementForWidgetOfExactType(type)?.widget; return provider?.bloc; }}class _BlocProviderState
extends State
>{ @override void dispose(){ widget.bloc?.dispose(); super.dispose(); } @override Widget build(BuildContext context){ return new _BlocProviderInherited
( bloc: widget.bloc, child: widget.child, ); }}class _BlocProviderInherited
extends InheritedWidget { _BlocProviderInherited({ Key key, @required Widget child, @required this.bloc, }) : super(key: key, child: child); final T bloc; @override bool updateShouldNotify(_BlocProviderInherited oldWidget) => false;}复制代码

这种解决方式的优点是 性能 更高。

由于使用了 InheritedWidget,现在可以调用 context.ancestorInheritedElementForWidgetOfExactType() 方法,它是一个 O(1) 复杂度的方法,这意味着获取祖先节点是非常快的,如其源代码所示:

@overrideInheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) {    assert(_debugCheckStateIsActiveForAncestorLookup());    final InheritedElement ancestor = _inheritedWidgets == null                                     ? null                                     : _inheritedWidgets[targetType];    return ancestor;}复制代码

这也表明所有 InheritedWidgets 都由 Framework 保存。

为什么要使用 ancestorInheritedElementForWidgetOfExactType 呢 ?

你应该已经注意到了我用 ancestorInheritedElementForWidgetOfExactType 代替了通常使用的 inheritFromWidgetOfExactType 方法。

原因是我不希望调用 BlocProvider 的上下文被注册为 InheritedWidget 的依赖项,因为我不需要它。

1.3. 如何使用新的 BlocProvider

1.3.1. 注入 BLoC

Widget build(BuildContext context){    return BlocProvider
{ bloc: myBloc, child: ... }}复制代码

1.3.2. BLoC 实例的获取

Widget build(BuildContext context){    MyBloc myBloc = BlocProvider.of
(context); ...}复制代码

2. 在哪里初始化BLoC

要回答这个问题,您需要弄清楚其使用范围。

2.1. 应用程序中任何地方都可用

假如您必须处理一些与用户身份验证或用户简介、用户首选项、购物车相关的一些业务逻辑…… 任何需要从应用程序的任何可能地方(例如,从不同页面)都可以 获取到 BLoC 的业务逻辑,有两种方式 可以使这个 BLoC 在任何地方都可以访问。

2.1.1. 使用全局单例

此解决方案依赖于使用全局对象,(为所有使用的地方)实例化一次,不是任何 Widget 树 的一部分。

import 'package:rxdart/rxdart.dart';class GlobalBloc {  ///  /// Streams related to this BLoC  ///  BehaviorSubject
_controller = BehaviorSubject
(); Function(String) get push => _controller.sink.add; Stream
get stream => _controller; /// /// Singleton factory /// static final GlobalBloc _bloc = new GlobalBloc._internal(); factory GlobalBloc(){ return _bloc; } GlobalBloc._internal(); /// /// Resource disposal /// void dispose(){ _controller?.close();}GlobalBloc globalBloc = GlobalBloc();复制代码

要使用此 BLoC,您只需导入该类并直接调用其方法,如下所示:

import 'global_bloc.dart';class MyWidget extends StatelessWidget {    @override    Widget build(BuildContext context){        globalBloc.push('building MyWidget');        return Container();    }}复制代码

如果您需要一个 唯一BLoC 并需要从应用程序内部的任何位置访问,这是一个可接受的解决方案。

  • 这是很容易使用的
  • 它不依赖任何 BuildContext
  • 不需要通过 BlocProvider 查找 BLoC,并且
  • 为了释放它的资源,只需确保将应用程序实现为 StatefulWidget,并在 StatefulWidget 重写的 dispose() 方法中调用 globalBloc.dispose() 即可。

许多纯粹主义者反对这种解决方案。 我不知道为什么,但是…所以让我们看看另一个实现方式吧 ......

2.1.2. 把它放在所有的 Widget 之上

在 Flutter 中,所有页面的祖先本身必须是 MaterialApp 的父级 。这是因为一个页面(或路径)是被包装在 一个 OverlayEntry 中的,是所有页面 的一个子项。

换句话说,每个页面都有一个 独立于任何其他页面Buildcontext。 这就解释了为什么在不使用任何技巧的情况下,两个页面(或路由)不可能有任何共同的地方。

因此,如果您需要在应用程序中的任何位置使用 BLoC,则必须将其作为 MaterialApp 的父级,如下所示:

void main() => runApp(Application());class Application extends StatelessWidget {  @override  Widget build(BuildContext context) {    return BlocProvider
( bloc: AuthenticationBloc(), child: MaterialApp( title: 'BLoC Samples', theme: ThemeData( primarySwatch: Colors.blue, ), home: InitializationPage(), ), ); }}复制代码

2.2. 在一个子树中可用

在大多数情况下,您可能需要在应用程序的某些特定部分使用某一个 BLoC

作为一个例子,我们可以想象一个讨论相关的模块,它的 BLoC 将会用于:

  • 与服务器交互以 获取、添加、修改帖子。
  • 在某一个页面中列出所有讨论的话题。
  • ……

在这个例子中,你不需要使这个 BLoC 在整个应用的任何地方都可用,只需要在一些 Widget 中可用(树的一部分)。

第一种解决方案可能是将 BLoC 注入到 Widget 树 的根节点,如下所示:

class MyTree extends StatelessWidget {  @override  Widget build(BuildContext context){    return BlocProvider
( bloc: MyBloc(), child: Column( children:
[ MyChildWidget(), ], ), ); }}class MyChildWidget extends StatelessWidget { @override Widget build(BuildContext context){ MyBloc = BlocProvider.of
(context); return Container(); }}复制代码

这样,所有 Widget 都可用通过调用 BlocProvider.of 方法访问 BLoC

边注

如上所示的解决方案并不是最佳的,因为它将在每次重新构建(rebuild)时实例化BLoC。

后果:

  • 您将丢失 BLoC 的任何现有的内容
  • 它会耗费 CPU 时间,因为它需要在每次构建时实例化它。

在这种情况下,更好的方法是使用 StatefulWidget 从其持久状态中受益,如下所示:

class MyTree extends StatefulWidget { @override  _MyTreeState createState() => _MyTreeState();}class _MyTreeState extends State
{ MyBloc bloc; @override void initState(){ super.initState(); bloc = MyBloc(); } @override void dispose(){ bloc?.dispose(); super.dispose(); } @override Widget build(BuildContext context){ return BlocProvider
( bloc: bloc, child: Column( children:
[ MyChildWidget(), ], ), ); }}复制代码

使用这种方法,如果需要重新构建 “MyTree” Widget ,则不必重新实例化 BLoC 并直接重用现有实例。

2.3. 仅适用于一个小部件

这涉及到 一个 BLoC 仅由一个 Widget 使用的情况。 在这种情况下,可以在 Widget 中实例化 BLoC

3. 事件(Event) 、 状态(State)

有时,处理一系列可能是顺序或并行,长或短,同步或异步以及可能导致各种结果的操作可能变得非常难以编程。您可能还需要根据状态的改变或进度更新显示。

此第一个例子旨在使这种情况更容易处理。

该解决方案基于以下原则:

  • 一个事件被发出;
  • 这个事件触发一些导致一个或多个状态的动作;
  • 这些状态中的每一个都可以反过来发出其他事件或导致另一个状态;
  • 然后,这些事件将根据活动状态触发其他操作;
  • 等等…

为了说明这个概念,我们来看两个常见的例子:

  • 应用初始化

    假设您需要执行一系列操作来初始化一个应用程序。 这些操作可能有与服务器的交互(例如,加载一些数据)。 在此初始化过程中,您可能需要显示进度条和一系列图像以使用户等待。

  • 用户认证 在启动时,应用程序可能要求用户进行身份验证或注册。 用户通过身份验证后,将重定向到应用程序的主页面。 然后,如果用户注销,则将其重定向到认证页面。

为了能够处理所有可能的情况,事件序列,并且如果我们认为可以在应用程序中的任何地方触发这些事件,这可能变得非常难以管理。

这就是 BlocEventStateBlocEventStateBuilder 相结合可以有很大帮助的地方……

3.1. BlocEventState

BlocEventState 背后的思想是定义这样一个 BLoC

  • 接受 事件(Event) 作为输入;
  • 在发出新事件时调用 eventHandler;
  • eventHandler 负责根据 事件 采取适当的操作并发出 状态 作为回应。

下图显示了这个思想:

这是这个类的源代码。 解释在代码后面:

import 'package:blocs/bloc_helpers/bloc_provider.dart';import 'package:meta/meta.dart';import 'package:rxdart/rxdart.dart';abstract class BlocEvent extends Object {}abstract class BlocState extends Object {}abstract class BlocEventStateBase
implements BlocBase { PublishSubject
_eventController = PublishSubject
(); BehaviorSubject
_stateController = BehaviorSubject
(); /// /// To be invoked to emit an event /// Function(BlocEvent) get emitEvent => _eventController.sink.add; /// /// Current/New state /// Stream
get state => _stateController.stream; /// /// External processing of the event /// Stream
eventHandler(BlocEvent event, BlocState currentState); /// /// initialState /// final BlocState initialState; // // Constructor // BlocEventStateBase({ @required this.initialState, }){ // // For each received event, we invoke the [eventHandler] and // emit any resulting newState // _eventController.listen((BlocEvent event){ BlocState currentState = _stateController.value ?? initialState; eventHandler(event, currentState).forEach((BlocState newState){ _stateController.sink.add(newState); }); }); } @override void dispose() { _eventController.close(); _stateController.close(); }}复制代码

如您所见,这是一个需要继承的抽象类,用于定义 eventHandler 方法的行为。

它暴露了:

  • 一个 Sink(emitEvent) 来推送一个 事件;
  • 一个 Stream (state) 来监听发送的 状态

在初始化时(请参阅构造函数):

  • 需要提供一个 initialState;
  • 它创建一个 StreamSubscription 来监听传入的 事件
    • 将它们发送到 eventHandler
    • 发出结果 状态

3.2. 专门的 BlocEventState

用于实现此 BlocEventState 的泛型类在下面给出。 之后,我们将实现一个真实的类。

class TemplateEventStateBloc extends BlocEventStateBase
{ TemplateEventStateBloc() : super( initialState: BlocState.notInitialized(), ); @override Stream
eventHandler( BlocEvent event, BlocState currentState) async* { yield BlocState.notInitialized(); }}复制代码

如果这个泛型类不能通过编译,请不要担心……这是正常的,因为我们还没有定义 BlocState.notInitialized() …… 这将在几分钟内出现。

此泛型类仅在初始化时提供 initialState 并重写了 eventHandler

这里有一些非常有趣的事情需要注意。 我们使用了异步生成器:**async *** 和 yield 语句。

使用 async* 修饰符标记函数,将函数标识为异步生成器:

每次调用 yield 语句时,它都会将 yield 后面的表达式结果添加到输出 Stream 中。

如果我们需要通过一系列操作发出一系列状态(我们稍后会在实践中看到),这将特别有用

有关异步生成器的其他详细信息,请点击此。

3.3. BlocEvent 和 BlocState

正如您所注意到的,我们已经定义了一个 BlocEventBlocState 抽象类。

这些类需要你使用想要发出的特定的事件和状态去 继承

3.4. BlocEventStateBuilder Widget

这个模式的最后一部分是 BlocEventStateBuilder Widget,它允许您响应 BlocEventState 发出的 State

这是它的源代码:

typedef Widget AsyncBlocEventStateBuilder
(BuildContext context, BlocState state);class BlocEventStateBuilder
extends StatelessWidget { const BlocEventStateBuilder({ Key key, @required this.builder, @required this.bloc, }): assert(builder != null), assert(bloc != null), super(key: key); final BlocEventStateBase
bloc; final AsyncBlocEventStateBuilder
builder; @override Widget build(BuildContext context){ return StreamBuilder
( stream: bloc.state, initialData: bloc.initialState, builder: (BuildContext context, AsyncSnapshot
snapshot){ return builder(context, snapshot.data); }, ); }}复制代码

这个 Widget 只是一个专门的 StreamBuilder,它会在每次发出新的 BlocState 时调用传入的 builder 参数。

OK,现在我们已经拥有了 EventStateBloc 设计模式 所有的部分了,现在是时候展示我们可以用它们做些什么了......

3.5. 案例1:应用程序初始化

第一个示例说明了您需要应用程序在启动时执行某些任务的情况。

通常的用途是,游戏在显示实际主屏幕之前,最初显示启动画面(动画与否),同时从服务器获取一些文件,检查新的更新是否可用,尝试连接到任何游戏中心……。为了不给用户程序什么都没做的感觉,它可能会显示一个进度条并定期显示一些图片,同时它会完成所有初始化过程。

我要展示的具体实现非常简单。 它只会在屏幕上显示一些完成百分比,但这可以根据你的需求很容易地扩展。

首先要做的是定义事件和状态......

3.5.1. ApplicationInitializationEvent

在这个例子中,我只考虑2个事件:

  • start:此事件将触发初始化过程;
  • stop:该事件可用于强制停止初始化进程。

这是定义:

class ApplicationInitializationEvent extends BlocEvent {    final ApplicationInitializationEventType type;  ApplicationInitializationEvent({    this.type: ApplicationInitializationEventType.start,  }) : assert(type != null);}enum ApplicationInitializationEventType {  start,  stop,}复制代码

3.5.2. ApplicationInitializationState

这个类将提供与初始化过程相关的信息。

对于这个例子,我会考虑:

  • 2个标志:

    • isInitialized 指示初始化是否完成
    • isInitializing 以了解我们是否处于初始化过程的中间
  • 进度完成率

这是代码:

class ApplicationInitializationState extends BlocState {  ApplicationInitializationState({    @required this.isInitialized,    this.isInitializing: false,    this.progress: 0,  });  final bool isInitialized;  final bool isInitializing;  final int progress;  factory ApplicationInitializationState.notInitialized() {    return ApplicationInitializationState(      isInitialized: false,    );  }  factory ApplicationInitializationState.progressing(int progress) {    return ApplicationInitializationState(      isInitialized: progress == 100,      isInitializing: true,      progress: progress,    );  }  factory ApplicationInitializationState.initialized() {    return ApplicationInitializationState(      isInitialized: true,      progress: 100,    );  }}复制代码

3.5.3. ApplicationInitializationBloc

BLoC 负责处理基于事件的初始化过程。

这是代码:

class ApplicationInitializationBloc    extends BlocEventStateBase
{ ApplicationInitializationBloc() : super( initialState: ApplicationInitializationState.notInitialized(), ); @override Stream
eventHandler( ApplicationInitializationEvent event, ApplicationInitializationState currentState) async* { if (!currentState.isInitialized){ yield ApplicationInitializationState.notInitialized(); } if (event.type == ApplicationInitializationEventType.start) { for (int progress = 0; progress < 101; progress += 10){ await Future.delayed(const Duration(milliseconds: 300)); yield ApplicationInitializationState.progressing(progress); } } if (event.type == ApplicationInitializationEventType.stop){ yield ApplicationInitializationState.initialized(); } }}复制代码

一些解释:

  • 当收到 “ApplicationInitializationEventType.start” 事件时,它从0到100开始计数(步骤10),并且对于每个值(0,10,20,……),它发出(通过yield)一个新状态,以通知 BLoC 初始化正在进行中(isInitializing = true)及其进度值。

  • 当收到 “ApplicationInitializationEventType.stop” 事件时,它认为初始化已完成。

  • 正如你所看到的,我在计数器循环中设置了一些延迟。 这将向您展示如何使用任何 Future(例如,您需要联系服务器的情况)

3.5.4. 将它们全部包装在一起

现在,剩下的部分是显示 计数器的假的启动画面 ......

class InitializationPage extends StatefulWidget {  @override  _InitializationPageState createState() => _InitializationPageState();}class _InitializationPageState extends State
{ ApplicationInitializationBloc bloc; @override void initState(){ super.initState(); bloc = ApplicationInitializationBloc(); bloc.emitEvent(ApplicationInitializationEvent()); } @override void dispose(){ bloc?.dispose(); super.dispose(); } @override Widget build(BuildContext pageContext) { return SafeArea( child: Scaffold( body: Container( child: Center( child: BlocEventStateBuilder
( bloc: bloc, builder: (BuildContext context, ApplicationInitializationState state){ if (state.isInitialized){ // // 一旦初始化完成,跳转到其他页面 // WidgetsBinding.instance.addPostFrameCallback((_){ Navigator.of(context).pushReplacementNamed('/home'); }); } return Text('Initialization in progress... ${state.progress}%'); }, ), ), ), ), ); }}复制代码

说明:

  • 由于 ApplicationInitializationBloc 不需要在应用程序的任何地方使用,我们可以在一个 StatefulWidget 中初始化它;

  • 我们直接发出 ApplicationInitializationEventType.start 事件来触发 eventHandler

  • 每次发出 ApplicationInitializationState 时,我们都会更新文本

  • 初始化完成后,我们将用户重定向到主页。

技巧

由于我们无法直接在构建器内部重定向到主页,我们使用 WidgetsBinding.instance.addPostFrameCallback() 方法请求 Flutter 在渲染完成后立即执行方法

3.6 案例2:应用程序身份验证和退出

对于此示例,我将考虑以下用例:

  • 在启动时,如果用户未经过身份验证,则会自动显示“身份验证/注册”页面;

  • 在用户认证期间,显示 CircularProgressIndicator;

  • 经过身份验证后,用户将被重定向到主页;

  • 在应用程序的任何地方,用户都可以注销;

  • 当用户注销时,用户将自动重定向到“身份验证”页面。

当然,很有可能以编程方式处理所有这些,但将所有这些委托给 BLoC 要容易得多。

下图解释了我要讲解的解决方案:

名为 “DecisionPage” 的中间页面将负责将用户自动重定向到“身份验证”页面或主页,具体取决于用户身份验证的状态。 当然,此 DecisionPage 从不显示,也不应被视为页面。

首先要做的是定义事件和状态......

3.6.1. AuthenticationEvent

在这个例子中,我只考虑2个事件:

  • login:当用户正确认证时发出此事件;
  • logout:用户注销时发出的事件。

这是定义:

abstract class AuthenticationEvent extends BlocEvent {  final String name;  AuthenticationEvent({    this.name: '',  });}class AuthenticationEventLogin extends AuthenticationEvent {  AuthenticationEventLogin({    String name,  }) : super(          name: name,        );}class AuthenticationEventLogout extends AuthenticationEvent {}复制代码

3.6.2. AuthenticationState

该类将提供与身份验证过程相关的信息。

对于这个例子,我将考虑:

  • 3个标志:

    • isAuthenticated 指示身份验证是否完整
    • isAuthenticating 以了解我们是否处于身份验证过程的中间
    • hasFailed 表示身份验证失败
  • 经过身份验证的用户名

这是它的源代码:

class AuthenticationState extends BlocState {  AuthenticationState({    @required this.isAuthenticated,    this.isAuthenticating: false,    this.hasFailed: false,    this.name: '',  });  final bool isAuthenticated;  final bool isAuthenticating;  final bool hasFailed;  final String name;    factory AuthenticationState.notAuthenticated() {    return AuthenticationState(      isAuthenticated: false,    );  }  factory AuthenticationState.authenticated(String name) {    return AuthenticationState(      isAuthenticated: true,      name: name,    );  }  factory AuthenticationState.authenticating() {    return AuthenticationState(      isAuthenticated: false,      isAuthenticating: true,    );  }  factory AuthenticationState.failure() {    return AuthenticationState(      isAuthenticated: false,      hasFailed: true,    );  }}复制代码

3.6.3. AuthenticationBloc

BLoC 负责根据事件处理身份验证过程。

这是代码:

class AuthenticationBloc    extends BlocEventStateBase
{ AuthenticationBloc() : super( initialState: AuthenticationState.notAuthenticated(), ); @override Stream
eventHandler( AuthenticationEvent event, AuthenticationState currentState) async* { if (event is AuthenticationEventLogin) { // Inform that we are proceeding with the authentication yield AuthenticationState.authenticating(); // Simulate a call to the authentication server await Future.delayed(const Duration(seconds: 2)); // Inform that we have successfuly authenticated, or not if (event.name == "failure"){ yield AuthenticationState.failure(); } else { yield AuthenticationState.authenticated(event.name); } } if (event is AuthenticationEventLogout){ yield AuthenticationState.notAuthenticated(); } }}复制代码

一些解释:

  • 当收到 “AuthenticationEventLogin” 事件时,它会(通过 yield )发出一个新状态,告知身份验证正在执行(isAuthenticating = true)。
  • 然后它运行身份验证,一旦完成,就会发出另一个状态,告知身份验证已完成。
  • 当收到 “AuthenticationEventLogout” 事件时,它将发出一个新状态,告诉程序用户已退出认证。

3.6.4. AuthenticationPage

正如您将要看到的那样,为了便于解释,此页面非常基本且不会做太多内容。

这是代码。 解释稍后给出:

class AuthenticationPage extends StatelessWidget {  ///  /// Prevents the use of the "back" button  ///  Future
_onWillPopScope() async { return false; } @override Widget build(BuildContext context) { AuthenticationBloc bloc = BlocProvider.of
(context); return WillPopScope( onWillPop: _onWillPopScope, child: SafeArea( child: Scaffold( appBar: AppBar( title: Text('Authentication Page'), leading: Container(), ), body: Container( child: BlocEventStateBuilder
( bloc: bloc, builder: (BuildContext context, AuthenticationState state) { if (state.isAuthenticating) { return PendingAction(); } if (state.isAuthenticated){ return Container(); } List
children =
[]; // Button to fake the authentication (success) children.add( ListTile( title: RaisedButton( child: Text('Log in (success)'), onPressed: () { bloc.emitEvent(AuthenticationEventLogin(name: 'Didier')); }, ), ), ); // Button to fake the authentication (failure) children.add( ListTile( title: RaisedButton( child: Text('Log in (failure)'), onPressed: () { bloc.emitEvent(AuthenticationEventLogin(name: 'failure')); }, ), ), ); // Display a text if the authentication failed if (state.hasFailed){ children.add( Text('Authentication failure!'), ); } return Column( children: children, ); }, ), ), ), ), ); }}复制代码

说明:

  • 第11行:页面获取对 AuthenticationBloc 的引用
  • 第24-70行:它监听发出的 AuthenticationState
    • 如果身份验证正在进行中,它会显示一个 CircularProgressIndicator ,告诉用户正在进行某些操作并阻止用户操作此页面(第25-27行)
    • 如果验证成功,我们不需要显示任何内容(第29-31行)。
    • 如果用户未经过身份验证,则会显示2个按钮以模拟成功的身份验证和失败。
    • 当我们点击其中一个按钮时,我们发出一个 AuthenticationEventLoginevent,以及一些参数(通常由认证过程使用)
    • 如果验证失败,我们会显示错误消息(第60-64行)

就是这样! 没有别的事情需要做了……很简单,不是吗?

提示:

您可能已经注意到,我将页面包装在 WillPopScope 中。 理由是我不希望用户能够使用 Android '后退' 按钮,如此示例中所示,身份验证是一个必须的步骤,它阻止用户访问任何其他部分,除非经过正确的身份验证。

3.6.5. DecisionPage

如前所述,我希望应用程序根据身份验证状态自动重定向到 AuthenticationPageHomePage

以下是此 DecisionPage 的代码,说明在代码后面:

class DecisionPage extends StatefulWidget {  @override  DecisionPageState createState() {    return new DecisionPageState();  }}class DecisionPageState extends State
{ AuthenticationState oldAuthenticationState; @override Widget build(BuildContext context) { AuthenticationBloc bloc = BlocProvider.of
(context); return BlocEventStateBuilder
( bloc: bloc, builder: (BuildContext context, AuthenticationState state) { if (state != oldAuthenticationState){ oldAuthenticationState = state; if (state.isAuthenticated){ _redirectToPage(context, HomePage()); } else if (state.isAuthenticating || state.hasFailed){ //do nothing } else { _redirectToPage(context, AuthenticationPage()); } } // This page does not need to display anything since it will // always remind behind any active page (and thus 'hidden'). return Container(); } ); } void _redirectToPage(BuildContext context, Widget page){ WidgetsBinding.instance.addPostFrameCallback((_){ MaterialPageRoute newRoute = MaterialPageRoute( builder: (BuildContext context) => page ); Navigator.of(context).pushAndRemoveUntil(newRoute, ModalRoute.withName('/decision')); }); }}复制代码

提醒

为了详细解释这一点,我们需要回到Flutter处理Pages(= Route)的方式。 要处理路由,我们使用 Navigator,它创建一个 Overlay

这个 Overlay 是一个 OverlayEntry 堆栈,每个都包含一个 Page。

当我们通过 Navigator.of(context) 压入,弹出,替换页面时,后者更新其 重新构建(rebuild)Overlay (此堆栈)。

重新构建堆栈时,每个 OverlayEntry(包括其内容) 也会 重新构建

因此,当我们通过 Navigator.of(context) 进行操作时,所有剩余的页面都会重新构建

  • 那么,为什么我将它实现为 StatefulWidget

    为了能够响应 AuthenticationState 的任何更改,此 “页面” 需要在应用程序的整个生命周期中保持存在。

    这意味着,根据上面的提醒,每次 Navigator.of(context) 完成操作时,都会重新构建此页面

    因此,它的 BlocEventStateBuilder 也将重建,调用自己的 builder 方法。

    因为此 builder 负责将用户重定向到与 AuthenticationState 对应的页面,所以如果我们每次重新构建页面时重定向用户,它将继续重定向,因为不断地重新构建。

    为了防止这种情况发生,我们只需要记住我们采取操作的最后一个 AuthenticationState,并且只在收到另一个 AuthenticationState 时采取另一个动作。

  • 这是如何起作用的?

    如上所述,每次发出AuthenticationState 时,BlocEventStateBuilder 都会调用其 builder

    基于状态标志(isAuthenticated),我们知道我们需要向哪个页面重定向用户。

技巧

由于我们无法直接从构建器重定向到另一个页面,因此我们使用WidgetsBinding.instance.addPostFrameCallback() 方法在呈现完成后请求 Flutter 执行方法

此外,由于我们需要在重定向用户之前删除任何现有页面,除了需要保留在所有情况下的此 DecisionPage 之外,我们使用 Navigator.of(context).pushAndRemoveUntil(…) 来实现此目的。

3.6.8. Log out

为了让用户退出,您现在可以创建一个 “LogOutButton” 并将其放在应用程序的任何位置。

此按钮只需要发出 AuthenticationEventLogout() 事件,这将导致以下自动操作链:

  1. 它将由AuthenticationBloc处理
  2. 反过来会发出一个AuthentiationState(isAuthenticated = false)
  3. 这将由 DecisionPage 通过 BlocEventStateBuilder 处理
  4. 这会将用户重定向到AuthenticationPage

这是此按钮的代码:

class LogOutButton extends StatelessWidget {  @override  Widget build(BuildContext context) {    AuthenticationBloc bloc = BlocProvider.of
(context); return IconButton( icon: Icon(Icons.exit_to_app), onPressed: () { bloc.emitEvent(AuthenticationEventLogout()); }, ); }}复制代码

3.6.9. AuthenticationBloc

由于 AuthenticationBloc 需要在此应用程序的任何页面可用,我们还是将其作为MaterialApp的父级注入,如下所示:

void main() => runApp(Application());class Application extends StatelessWidget {  @override  Widget build(BuildContext context) {    return BlocProvider
( bloc: AuthenticationBloc(), child: MaterialApp( title: 'BLoC Samples', theme: ThemeData( primarySwatch: Colors.blue, ), home: DecisionPage(), ), ); }}复制代码

4. 表单验证

BLoC 的另一个有趣应用是当您需要验证表单时:

  • 根据某些业务规则验证与 TextField 相关的输入;
  • 根据规则显示验证错误消息;
  • 根据业务规则自动化 Widget 的可访问性。

我现在要做的一个例子是 RegistrationForm,它由3个TextFields(电子邮件,密码,确认密码)和1个RaisedButton组成,以启动注册过程。

我想要实现的业务规则是:

  • 电子邮件需要是有效的电子邮件地址。 如果不是,则需要显示错误提示消息。
  • 密码必须有效(必须包含至少8个字符,1个大写,1个小写,1个数字和1个特殊字符)。 如果无效,则需要显示错误提示消息。
  • 重新输入密码需要符合相同的验证规则并且与密码相同。 如果不相同,则需要显示错误消息。
  • 注册按钮只有在所有规则有效时才有效。

4.1. The RegistrationFormBloc

此 BLoC 负责处理验证业务规则,如前所述。

这是它的源代码:

class RegistrationFormBloc extends Object with EmailValidator, PasswordValidator implements BlocBase {  final BehaviorSubject
_emailController = BehaviorSubject
(); final BehaviorSubject
_passwordController = BehaviorSubject
(); final BehaviorSubject
_passwordConfirmController = BehaviorSubject
(); // // Inputs // Function(String) get onEmailChanged => _emailController.sink.add; Function(String) get onPasswordChanged => _passwordController.sink.add; Function(String) get onRetypePasswordChanged => _passwordConfirmController.sink.add; // // Validators // Stream
get email => _emailController.stream.transform(validateEmail); Stream
get password => _passwordController.stream.transform(validatePassword); Stream
get confirmPassword => _passwordConfirmController.stream.transform(validatePassword) .doOnData((String c){ // If the password is accepted (after validation of the rules) // we need to ensure both password and retyped password match if (0 != _passwordController.value.compareTo(c)){ // If they do not match, add an error _passwordConfirmController.addError("No Match"); } }); // // Registration button Stream
get registerValid => Observable.combineLatest3( email, password, confirmPassword, (e, p, c) => true ); @override void dispose() { _emailController?.close(); _passwordController?.close(); _passwordConfirmController?.close(); }}复制代码

让我详细解释一下......

  • 我们首先初始化3个 BehaviorSubject 来处理表单的每个 TextFieldStreams
  • 我们公开了3个Function(String),它将用于接受来自 TextFields 的输入。
  • 我们公开了3个 Stream <String>TextField 将使用它来显示由它们各自的验证产生的潜在错误消息
  • 我们公开了1个 Stream <bool>,它将被 RaisedButton 使用,以根据整个验证结果启用/禁用它。

OK,现在是时候深入了解更多细节......

您可能已经注意到,此类的签名有点特殊。 我们来回顾一下吧。

class RegistrationFormBloc extends Object                            with EmailValidator, PasswordValidator                            implements BlocBase {  ...}复制代码

With 关键字表示此类正在使用 MIXINS(=“在另一个类中重用某些类代码的方法”),并且为了能够使用 with 关键字,该类需要继承 Object 类。 这些 mixin 分别包含验证电子邮件和密码的代码。

有关 Mixins 的更多详细信息,我建议您阅读 的这篇精彩文章。

4.1.1. Validator Mixins

我只会解释EmailValidator,因为PasswordValidator非常相似。

首先,代码:

const String _kEmailRule = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$";class EmailValidator {  final StreamTransformer
validateEmail = StreamTransformer
.fromHandlers(handleData: (email, sink){ final RegExp emailExp = new RegExp(_kEmailRule); if (!emailExp.hasMatch(email) || email.isEmpty){ sink.addError('Entre a valid email'); } else { sink.add(email); } });}复制代码

该类公开了一个 final 函数(“validateEmail”),它是一个 StreamTransformer

提醒

StreamTransformer 的调用方式如下:stream.transform(StreamTransformer)

StreamTransformer 从 Stream 通过 transformmethod 引用它的输入。 然后处理此输入,并将转换后的输入重新注入初始 Stream。

在此代码中,输入的处理包括根据正则表达式进行检查。 如果输入与正则表达式匹配,我们只需将输入重新注入流中,否则,我们会向流中注入错误消息。

4.1.2. 为什么使用stream.transform()?

如前所述,如果验证成功,StreamTransformer 会将输入重新注入 Stream。 为什么这样做是有用的?

以下是与 Observable.combineLatest3() 相关的解释…此方法在它引用的所有 Streams 至少发出一个值之前不会发出任何值。

让我们看看下面的图片来说明我们想要实现的目标。

  • 如果用户输入电子邮件并且后者经过验证,它将由电子邮件流发出,它将是 Observable.combineLatest3() 的一个输入;

  • 如果电子邮件无效,则会向流中添加错误(并且流中没有值);

  • 这同样适用于密码和重新输入密码;

  • 当所有这三个验证都成功时(意味着所有这三个流都会发出一个值),Observable.combineLatest3() 将由 “(e,p,c)=> true”,发出一个true(见 第35行)。

4.1.2. 两个密码的验证

我在互联网上看到了很多与这种比较有关的问题。 存在几种解决方案,让我解释其中的两种。

4.1.2.1. 基本解决方案 - 没有错误消息

第一个解决方案可能是下面这样的:

Stream
get registerValid => Observable.combineLatest3( email, password, confirmPassword, (e, p, c) => (0 == p.compareTo(c)) );复制代码

这种解决方案简单地比较这两个密码,当它们验证通过且相互匹配,发出一个值(= true)。

我们很快就会看到,Register 按钮的可访问性将取决于registerValid 流。

如果两个密码不匹配,那个 Stream 不会发出任何值,并且 Register 按钮保持不活动状态,但用户不会收到任何错误消息以帮助他了解原因。

4.1.2.2. 带错误消息的解决方案

另一种解决方案包括扩展 confirmPassword 流的处理,如下所示:

Stream
get confirmPassword => _passwordConfirmController.stream.transform(validatePassword) .doOnData((String c){ // If the password is accepted (after validation of the rules) // we need to ensure both password and retyped password match if (0 != _passwordController.value.compareTo(c)){ // If they do not match, add an error _passwordConfirmController.addError("No Match"); } });复制代码

一旦重新输入密码验证通过,它就会被 Stream 发出,并且,通过使用 doOnData,我们可以直接获取此发出的值并将其与 password 流的值进行比较。 如果两者不匹配,我们现在可以发送错误消息。

4.2. RegistrationForm

现在让我们在解释前先看一下 RegistrationForm 的代码 :

class RegistrationForm extends StatefulWidget {  @override  _RegistrationFormState createState() => _RegistrationFormState();}class _RegistrationFormState extends State
{ RegistrationFormBloc _registrationFormBloc; @override void initState() { super.initState(); _registrationFormBloc = RegistrationFormBloc(); } @override void dispose() { _registrationFormBloc?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Form( child: Column( children:
[ StreamBuilder
( stream: _registrationFormBloc.email, builder: (BuildContext context, AsyncSnapshot
snapshot) { return TextField( decoration: InputDecoration( labelText: 'email', errorText: snapshot.error, ), onChanged: _registrationFormBloc.onEmailChanged, keyboardType: TextInputType.emailAddress, ); }), StreamBuilder
( stream: _registrationFormBloc.password, builder: (BuildContext context, AsyncSnapshot
snapshot) { return TextField( decoration: InputDecoration( labelText: 'password', errorText: snapshot.error, ), obscureText: false, onChanged: _registrationFormBloc.onPasswordChanged, ); }), StreamBuilder
( stream: _registrationFormBloc.confirmPassword, builder: (BuildContext context, AsyncSnapshot
snapshot) { return TextField( decoration: InputDecoration( labelText: 'retype password', errorText: snapshot.error, ), obscureText: false, onChanged: _registrationFormBloc.onRetypePasswordChanged, ); }), StreamBuilder
( stream: _registrationFormBloc.registerValid, builder: (BuildContext context, AsyncSnapshot
snapshot) { return RaisedButton( child: Text('Register'), onPressed: (snapshot.hasData && snapshot.data == true) ? () { // launch the registration process } : null, ); }), ], ), ); }}复制代码

说明:

  • 由于 RegisterFormBloc 仅供此表单使用,因此在此处初始化它是适合的。

  • 每个 TextField 都包装在 StreamBuilder <String> 中,以便能够响应验证过程的任何结果(请参阅 errorText:snapshot.error

  • 每次对 TextField 的内容进行修改时,我们都会通过 onChanged 发送输入到 BLoC 进行验证:_registrationFormBloc.onEmailChanged(电子邮件输入的情况)

  • 对于RegisterButton,也包含在 StreamBuilder <bool> 中。

    • 如果 _registrationFormBloc.registerValid 发出一个值,onPressed 方法将执行某些操作
    • 如果未发出任何值,onPressed 方法将被指定为 null,这将撤销该按钮的激活状态。

就这样! 表单中没有任何业务规则,这意味着可以更改规则而无需对表单进行任何修改,这样非常好!

5. Part Of (部分模式)

有些时候,对于一个 Widget,根据它是否存在于某一个集合中来驱动其行为是一件有趣的事。

对于本文的最后一个例子,我将考虑以下场景:

  • 应用程序处理商品;

  • 用户可以选择放入购物车的商品;

  • 一件商品只能放入购物车一次;

  • 存放在购物车中的商品可以从购物车中移除;

  • 一旦被移除,就可以将其再次添加到购物车中。

对于此例子,每个商品将显示为一个按钮,该按钮如何显示将取决于该商品是否存在于购物车中。 如果该商品没有添加到购物车中,按钮将允许用户将其添加到购物车中。 如果商品已经被添加到购物车中,该按钮将允许用户将其从购物车中删除。

为了更好地说明 “Part of” 模式,我将考虑以下架构:

  • 购物页面将显示所有可能的商品的列表;

  • 购物页面中的每个商品都会显示一个按钮,用于将商品添加到购物车或将其从购物车中删除,具体取决于其是否存在于在购物车中;

  • 如果购物页面中的商品被添加到购物车中,其按钮将自动更新以允许用户将其从购物车中删除(反之亦然),而无需重新构建购物页面

  • 另一个页面,购物车页,将列出购物车中的所有商品;

  • 可以从此页面中删除购物车中的任何商品。

边注

Part Of 这个名字是我个人取的名字。 这不是一个官方名称。

5.1. ShoppingBloc

正如您现在可以想象的那样,我们需要考虑一个专门用于处理所有商品的列表,以及存在于购物车中的商品的 BLoC。

这个BLoC可能如下所示:

class ShoppingBloc implements BlocBase {  // List of all items, part of the shopping basket  Set
_shoppingBasket = Set
(); // Stream to list of all possible items BehaviorSubject
> _itemsController = BehaviorSubject
>(); Stream
> get items => _itemsController; // Stream to list the items part of the shopping basket BehaviorSubject
> _shoppingBasketController = BehaviorSubject
>(seedValue:
[]); Stream
> get shoppingBasket => _shoppingBasketController; @override void dispose() { _itemsController?.close(); _shoppingBasketController?.close(); } // Constructor ShoppingBloc() { _loadShoppingItems(); } void addToShoppingBasket(ShoppingItem item){ _shoppingBasket.add(item); _postActionOnBasket(); } void removeFromShoppingBasket(ShoppingItem item){ _shoppingBasket.remove(item); _postActionOnBasket(); } void _postActionOnBasket(){ // Feed the shopping basket stream with the new content _shoppingBasketController.sink.add(_shoppingBasket.toList()); // any additional processing such as // computation of the total price of the basket // number of items, part of the basket... } // // Generates a series of Shopping Items // Normally this should come from a call to the server // but for this sample, we simply simulate // void _loadShoppingItems() { _itemsController.sink.add(List
.generate(50, (int index) { return ShoppingItem( id: index, title: "Item $index", price: ((Random().nextDouble() * 40.0 + 10.0) * 100.0).roundToDouble() / 100.0, color: Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0) .withOpacity(1.0), ); })); }}复制代码

唯一可能需要解释的方法是 _postActionOnBasket() 方法。 每次在购物车中添加或删除项目时,我们都需要“更新” _shoppingBasketController Stream 的内容,以便通知所有正在监听此 StreamWidgets 并能够更新或重新构建页面。

5.2. ShoppingPage

此页面非常简单,只显示所有的商品。

class ShoppingPage extends StatelessWidget {  @override  Widget build(BuildContext context) {    ShoppingBloc bloc = BlocProvider.of
(context); return SafeArea( child: Scaffold( appBar: AppBar( title: Text('Shopping Page'), actions:
[ ShoppingBasket(), ], ), body: Container( child: StreamBuilder
>( stream: bloc.items, builder: (BuildContext context, AsyncSnapshot
> snapshot) { if (!snapshot.hasData) { return Container(); } return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 1.0, ), itemCount: snapshot.data.length, itemBuilder: (BuildContext context, int index) { return ShoppingItemWidget( shoppingItem: snapshot.data[index], ); }, ); }, ), ), )); }}复制代码

说明:

  • AppBar 显示一个按钮:

    • 显示出现在购物车中的商品的数量

    • 单击时将用户重定向到购物车页面

  • 商品列表使用 GridView 构建,包含在 StreamBuilder <List <ShoppingItem >>

  • 每个商品对应一个 ShoppingItemWidget

5.3. 购物车页面 (ShoppingBasketPage)

此页面与商品列表(ShoppingPage)非常相似,只是 StreamBuilder 现在正在侦听由 ShoppingBloc 公开的 _shoppingBasket 流的变化。

5.4. ShoppingItemWidget 和 ShoppingItemBloc

Part Of 模式依赖于这两个元素的组合:

  • ShoppingItemWidget 负责:
    • 显示商品和
    • 显示用于将其添加到购物车中或从购物车中将其删除的按钮
  • ShoppingItemBloc 负责通知 ShoppingItemWidget 后者其是否存在于购物车中。

让我们看看他们如何一起工作......

5.4.1. ShoppingItemBloc

ShoppingItemBloc 由每个 ShoppingItemWidget 实例化,赋予它 “身份”

BLoC 监听 ShoppingBasket 流的所有变化,并检测特定商品是否是存在于购物车中。

如果是,它会发出一个布尔值(= true),此值将被 ShoppingItemWidget 捕获,也就知道它是否存在于购物车中。

这是 BLoC 的代码:

class ShoppingItemBloc implements BlocBase {  // Stream to notify if the ShoppingItemWidget is part of the shopping basket  BehaviorSubject
_isInShoppingBasketController = BehaviorSubject
(); Stream
get isInShoppingBasket => _isInShoppingBasketController; // Stream that receives the list of all items, part of the shopping basket PublishSubject
> _shoppingBasketController = PublishSubject
>(); Function(List
) get shoppingBasket => _shoppingBasketController.sink.add; // Constructor with the 'identity' of the shoppingItem ShoppingItemBloc(ShoppingItem shoppingItem){ // Each time a variation of the content of the shopping basket _shoppingBasketController.stream // we check if this shoppingItem is part of the shopping basket .map((list) => list.any((ShoppingItem item) => item.id == shoppingItem.id)) // if it is part .listen((isInShoppingBasket) // we notify the ShoppingItemWidget => _isInShoppingBasketController.add(isInShoppingBasket)); } @override void dispose() { _isInShoppingBasketController?.close(); _shoppingBasketController?.close(); }}复制代码

5.4.2. ShoppingItemWidget

此 Widget 负责:

  • 创建 一个 ShoppingItemBloc 实例并将自己的商品标识传递给BLoC

  • 监听 ShoppingBasket 内容的任何变化并将其转移到BLoC

  • 监听 ShoppingItemBloc 以判断它是否存在于购物车中

  • 显示相应的按钮(添加/删除),具体取决于它是否存在于购物车中

  • 响应按钮的用户操作

    • 当用户点击添加按钮时,将自己添加到购物篮中
    • 当用户点击删除按钮时,将自己从篮子中移除。

让我们看看它是如何工作的(解释在代码中给出)。

class ShoppingItemWidget extends StatefulWidget {  ShoppingItemWidget({    Key key,    @required this.shoppingItem,  }) : super(key: key);  final ShoppingItem shoppingItem;  @override  _ShoppingItemWidgetState createState() => _ShoppingItemWidgetState();}class _ShoppingItemWidgetState extends State
{ StreamSubscription _subscription; ShoppingItemBloc _bloc; ShoppingBloc _shoppingBloc; @override void didChangeDependencies() { super.didChangeDependencies(); // As the context should not be used in the "initState()" method, // prefer using the "didChangeDependencies()" when you need // to refer to the context at initialization time _initBloc(); } @override void didUpdateWidget(ShoppingItemWidget oldWidget) { super.didUpdateWidget(oldWidget); // as Flutter might decide to reorganize the Widgets tree // it is preferable to recreate the links _disposeBloc(); _initBloc(); } @override void dispose() { _disposeBloc(); super.dispose(); } // This routine is reponsible for creating the links void _initBloc() { // Create an instance of the ShoppingItemBloc _bloc = ShoppingItemBloc(widget.shoppingItem); // Retrieve the BLoC that handles the Shopping Basket content _shoppingBloc = BlocProvider.of
(context); // Simple pipe that transfers the content of the shopping // basket to the ShoppingItemBloc _subscription = _shoppingBloc.shoppingBasket.listen(_bloc.shoppingBasket); } void _disposeBloc() { _subscription?.cancel(); _bloc?.dispose(); } Widget _buildButton() { return StreamBuilder
( stream: _bloc.isInShoppingBasket, initialData: false, builder: (BuildContext context, AsyncSnapshot
snapshot) { return snapshot.data ? _buildRemoveFromShoppingBasket() : _buildAddToShoppingBasket(); }, ); } Widget _buildAddToShoppingBasket(){ return RaisedButton( child: Text('Add...'), onPressed: (){ _shoppingBloc.addToShoppingBasket(widget.shoppingItem); }, ); } Widget _buildRemoveFromShoppingBasket(){ return RaisedButton( child: Text('Remove...'), onPressed: (){ _shoppingBloc.removeFromShoppingBasket(widget.shoppingItem); }, ); } @override Widget build(BuildContext context) { return Card( child: GridTile( header: Center( child: Text(widget.shoppingItem.title), ), footer: Center( child: Text('${widget.shoppingItem.price} €'), ), child: Container( color: widget.shoppingItem.color, child: Center( child: _buildButton(), ), ), ), ); }}复制代码

5.5. 这一切是如何运作的?

下图显示了所有部分如何协同工作。

小结

这是又一篇长文章,我本来希望我能简短一点,但我认为,为了阐述得更清楚,这么长也是值得的。

正如我在简介中所说的,我个人在我的开发中经常使用这些“模式”。 这让我节省了大量的时间和精力; 我的代码更易读,更容易调试。

此外,它有助于将业务与视图分离。

很有可能其他的方法也可以做到这些,甚至是更好的实现方式,但它对我是有用的,这就是我想与你分享的一切。

请继续关注新文章,同时祝您编程愉快。

转载地址:http://wxasa.baihongyu.com/

你可能感兴趣的文章
AWS - Serverless 和 lambda
查看>>
sqlserver 插入日文,俄文等语言显示乱码的解决方法
查看>>
我的友情链接
查看>>
Spring字符集过滤器CharacterEncodingFilter
查看>>
python脚本批量更改ESXI主机名,DNS
查看>>
npm安装rabbit.js
查看>>
Azkaban简介
查看>>
SimpleFolderEncrypt 使用帮助
查看>>
我的网管生活
查看>>
关于mysql官网下载不了Linux版本的问题
查看>>
Python编程快速上手让繁琐工作自动化-第十一章实践项目11.11.3 2048
查看>>
java对象与引用
查看>>
收起键盘的2种方法
查看>>
LVS负载均衡算法
查看>>
Weave Net在改变主机名后需要reset
查看>>
Zeppelin Notebook 存储访问 REST API
查看>>
Fabric on Kubernetes using Docker-in-Docker
查看>>
Python编码详解----转自AstralWind的Python字符串编码详解
查看>>
Linux之根文件系统
查看>>
可爱行销力量大:可爱的冰淇淋勺会让消费者想一直挖、一直挖
查看>>