博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Flutter-BLoC-第二讲
阅读量:6564 次
发布时间:2019-06-24

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

本篇已同步到 ,欢迎常来。

[译文]Reactive Programming - Streams - BLoC实际用例

介绍

在介绍了BLoC,Reactive Programming和Streams的概念后,我在一段时间之前做了一些介绍,尽管与我分享一些我经常使用并且个人觉得非常有用的模式(至少对我而言)可能会很有趣。这些模式使我在开发过程中节省了大量时间,并使我的代码更易于阅读和调试。

我要谈的话题是:

  • 1.BLoC Provider and InheritedWidget
  • 2.在哪里初始化BLoC?
  • 3.事件状态(允许根据事件响应状态转换)
  • 4.表格验证(允许根据条目和验证控制表单的行为)
  • 5.Part Of(允许Widget根据其在列表中的存在来调整其行为)

完整的源代码可以在上找到。

1.BLoC Provider and InheritedWidget

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

使用InheritedWidget的优点是我们获得了性能。

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)的函数,为了检索某种类型的祖先,它将对widget树 做向上导航,从上下文开始,一次递增一个父,直到完成。如果从上下文到祖先的距离很小(即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.使用全局单例
import 'package:rxdart/rxdart.dart';class GlobalBloc {  ///  /// 与此BLoC相关的流  ///  BehaviorSubject
_controller = BehaviorSubject
(); Function(String) get push => _controller.sink.add; Stream
get stream => _controller; /// /// Singleton工厂 /// 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,并在应用程序Widget 的重写dispose()方法中调用globalBloc.dispose()

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

2.1.2. 把它放在一切之上

在Flutter中,所有页面的祖先本身必须是MaterialApp的父级。这是由于这样的事实,一个页面(或路由)被包装在一个OverlayEntry,一个共同的孩子堆栈的所有页面。

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

因此,如果您需要在应用程序中的任何位置使用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,则必须将其作为MaterialApp的父级,如下所示:

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(); }}复制代码

这样一来,所有widgets都可以通过对呼叫BlocProvider.of方法 访问BLoC

附: 如上所示的解决方案并不是最佳解决方案,因为它将在每次重建时实例化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 ”小部件,则不必重新实例化BLoC并直接重用现有实例。

2.3.仅适用于一个小部件

这涉及BLoC仅由一个 Widget使用的情况。

在这种情况下,可以在Widget中实例化BLoC。

3.事件状态(允许根据事件响应状态转换)

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

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

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

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

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

应用初始化

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

认证

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

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

这就是BlocEventState,兼有BlocEventStateBuilder,可以帮助很多...

3.1. BlocEventState

BlocEventState背后的想法是定义一个BLoC:

  • 接受事件作为输入;
  • 当发出新事件时调用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
(); /// /// 要调用以发出事件 /// Function(BlocEvent) get emitEvent => _eventController.sink.add; /// /// 当前/新状态 /// Stream
get state => _stateController.stream; /// /// 事件的外部处理 /// Stream
eventHandler(BlocEvent event, BlocState currentState); /// /// initialState /// final BlocState initialState; // // 构造函数 // BlocEventStateBase({ @required this.initialState, }){ // // 对于每个接收到的事件,我们调用[eventHandler]并发出任何结果的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)来推送一个事件 ;
  • 一个流(状态)来监听发射状态。

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

一个初始化状态需要设置;

  • 它创建了一个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。

这是非常有用的,如果我们需要发出一个序列的States,从一系列的行动所造成(我们将在后面看到,在实践中)

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

3.3.BlocEvent和BlocState

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

这些类需要使用您要发出的特殊事件和状态进行扩展。

3.4. BlocEventStateBuilder小部件

模式最后一部分的是BlocEventStateBuilder小部件,它允许你在响应State(s),所发射的BlocEventState。

这是它的源代码:

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输入参数。


好的。现在我们已经拥有了所有的部分,现在是时候展示我们可以用它们做些什么......

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)一个告诉的新状态初始化正在运行(isInitializing = true)及其进度值。
  • 当收到事件"ApplicationInitializationEventType.stop"时,它认为初始化已完成。
  • 正如你所看到的,我在计数器循环中放了一些延迟。这将向您展示如何使用任何Future(例如,您需要联系服务器的情况)
3.5.4. 将它们全部包装在一起

现在,剩下的部分是显示显示计数器的伪Splash屏幕 ......

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){ // // Once the initialization is complete, let's move to another page // 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) { //通知我们正在进行身份验证 yield AuthenticationState.authenticating(); //模拟对身份验证服务器的调用 await Future.delayed(const Duration(seconds: 2)); //告知我们是否已成功通过身份验证 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个按钮以模拟成功的身份验证和失败。 当我们点击其中一个按钮时,我们发出一个AuthenticationEventLogin事件,以及一些参数(通常由认证过程使用) 如果验证失败,我们会显示错误消息(第60-64行)

提示

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

3.6.5. DecisionPage

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

以下是此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()); } }//此页面不需要显示任何内容 //总是在任何活动页面后面提醒(因此“隐藏”)。 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)的方式。要处理路由,我们使用导航器,它创建一个叠加层。 这个覆盖是一个堆栈的OverlayEntry,他们每个人的包含页面。 当我们通过Navigator.of(上下文)推送,弹出,替换页面时,后者更新其重建的覆盖(因此堆栈)。 当堆栈被重建,每个OverlayEntry(因此它的内容)也被重建。 因此,当我们通过Navigator.of(上下文)进行操作时,所有剩余的页面都会重建!

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

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

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

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

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

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

这是如何运作的?

如上所述,每次发出AuthenticationState时,BlocEventStateBuilder都会调用其构建器。

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

特技

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

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

3.6.6、登出 要让用户注销,您现在可以创建一个“ LogOutButton ”并将其放在应用程序的任何位置。

  • 此按钮只需要发出AuthenticationEventLogout()事件,这将导致以下自动操作链: 1.它将由AuthenticationBloc处理 2.反过来会发出一个AuthentiationState(isAuthenticated = false) 3.这将由DecisionPage通过BlocEventStateBuilder处理 4.这会将用户重定向到AuthenticationPage
3.6.7. 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相关的条目;
  • 根据规则显示验证错误消息;
  • 根据业务规则自动化窗口小部件的可访问性。

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

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

  • 该电子邮件必须是一个有效的电子邮件地址。如果不是,则需要显示消息。
  • 该密码必须是有效的(必须包含至少8个字符,具有1个大写,小写1,图1和1个特殊字符)。如果无效,则需要显示消息。
  • 在重新输入密码需要满足相同的验证规则和相同的密码。如果不相同,则需要显示消息。
  • 在登记时,按钮可能只能激活所有的规则都是有效的。

4.1.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来处理表单的每个TextField的Streams。
  • 我们公开了3个Function(String),它将用于接受来自TextFields的输入。
  • 我们公开了3个Stream ,TextField将使用它来显示由它们各自的验证产生的潜在错误消息
  • 我们公开了1个Stream ,它将被RaisedButton使用,以根据整个验证结果启用/禁用它。

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

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

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

with 关键字意味着这个类是使用混入(MIXINS)(在另一个类中重用一些类代码的一种方法),为了能够使用with关键字,该类需要扩展Object类。这些mixin包含分别验证电子邮件和密码的代码。

有关详细信息,混入我建议你阅读从这篇大文章 。

4.1.1. Validator Mixins

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

First, the code:

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通过transform方法从Stream引用它的输入。然后它处理此输入,并将转换后的输入重新注入初始Stream。

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

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

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

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

如果用户输入电子邮件并且后者经过验证,它将由电子邮件流发出,该电子邮件流将是Observable.combineLatest3()的一个输入; 如果电子邮件地址无效,错误将被添加到流(和没有价值会流出流); 这同样适用于密码和重新输入密码 ; 当所有这三个验证都成功时(意味着所有这三个流都会发出一个值),Observable.combineLatest3()将依次发出一个真正的感谢“ (e,p,c)=> true ”(见第35行)。

4.1.3. 验证2个密码

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

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

第一个解决方案可能是以下一个:

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

这个解决方案只需验证两个密码,如果它们匹配,就会发出一个值(= true)。

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

如果两个密码不匹配,则该流不会发出任何值,并且“ 注册”按钮保持不活动状态,但用户不会收到任何错误消息以帮助他理解原因。

4.1.3.2。带错误消息的解决方案

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

Stream
get confirmPassword => _passwordConfirmController.stream.transform(validatePassword) .doOnData((String c){ //如果接受密码(在验证规则后) //我们需要确保密码和重新输入的密码匹配 if (0 != _passwordController.value.compareTo(c)){ //如果它们不匹配,请添加错误 _passwordConfirmController.addError("No Match"); } });复制代码

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

4.2. The 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 中,以便能够响应验证过程的任何结果(请参阅errorText:snapshot.error)
  • 每次对TextField的内容进行修改时,我们都会通过onChanged发送输入到BLoC进行验证:_registrationFormBloc.onEmailChanged(电子邮件输入的情况)
  • 对于RegisterButton,后者也包含在StreamBuilder 中。
  • 如果_registrationFormBloc.registerValid发出一个值,onPressed方法将执行某些操作
  • 如果未发出任何值,则onPressed方法将被指定为null,这将取消激活该按钮。

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

5.Part Of(允许Widget根据其在列表中的存在来调整其行为)

有时,Widget知道它是否是驱动其行为的集合的一部分是有趣的。

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

应用程序处理项目; 用户可以选择放入购物篮的物品; 一件商品只能放入购物篮一次; 存放在购物篮中的物品可以从购物篮中取出; 一旦被移除,就可以将其取回。

对于此示例,每个项目将显示一个按钮,该按钮将取决于购物篮中物品的存在。如果不是购物篮的一部分,该按钮将允许用户将其添加到购物篮中。如果是购物篮的一部分,该按钮将允许用户将其从篮子中取出。

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

一个购物页面将显示所有可能的项目清单; 购物页面中的每个商品都会显示一个按钮,用于将商品添加到购物篮或将其移除,具体取决于其在购物篮中的位置; 如果一个项目在购物页被添加到篮,它的按钮将自动更新,以允许用户从所述篮(反之亦然)将其删除,而不必重新生成购物页 另一页,购物篮,将列出篮子里的所有物品; 可以从此页面中删除购物篮中的任何商品。

边注 Part Of这个名字是我给的个人名字。这不是官方名称。

正如您现在可以想象的那样,我们需要考虑一个专门用于处理所有可能项目列表的BLoC,以及购物篮的一部分。

这个BLoC可能如下所示:

class ShoppingBloc implements BlocBase {  // 所有商品的清单,购物篮的一部分  Set
_shoppingBasket = Set
(); // 流到所有可能项目的列表 BehaviorSubject
> _itemsController = BehaviorSubject
>(); Stream
> get items => _itemsController; // Stream以列出购物篮中的项目部分 BehaviorSubject
> _shoppingBasketController = BehaviorSubject
>(seedValue:
[]); Stream
> get shoppingBasket => _shoppingBasketController; @override void dispose() { _itemsController?.close(); _shoppingBasketController?.close(); } //构造函数 ShoppingBloc() { _loadShoppingItems(); } void addToShoppingBasket(ShoppingItem item){ _shoppingBasket.add(item); _postActionOnBasket(); } void removeFromShoppingBasket(ShoppingItem item){ _shoppingBasket.remove(item); _postActionOnBasket(); } void _postActionOnBasket(){ // 使用新内容提供购物篮流 _shoppingBasketController.sink.add(_shoppingBasket.toList()); // 任何其他处理,如 // 计算篮子的总价 // 项目数量,篮子的一部分...... } // //生成一系列购物项目 //通常这应该来自对服务器的调用 //但是对于这个样本,我们只是模拟 // 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 的内容,以便通知所有正在监听此Stream更改的Widgets并能够刷新/重建。

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显示按钮,: 显示出现在购物篮中的商品数量 单击时将用户重定向到ShoppingBasket页面
  • 项目列表使用GridView构建,包含在StreamBuilder <List >中
  • 每个项目对应一个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,如果ShoppingItemWidget是购物篮的一部分,则通知  BehaviorSubject
_isInShoppingBasketController = BehaviorSubject
(); Stream
get isInShoppingBasket => _isInShoppingBasketController; //收到所有商品列表的流,购物篮的一部分 PublishSubject
> _shoppingBasketController = PublishSubject
>(); Function(List
) get shoppingBasket => _shoppingBasketController.sink.add; //具有shoppingItem的“标识”的构造方法 ShoppingItemBloc(ShoppingItem shoppingItem){ //每次购物篮内容的变化 _shoppingBasketController.stream //我们检查这个shoppingItem是否是购物篮的一部分 .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(); //由于不应在“initState()”方法中使用上下文, //在需要时更喜欢使用“didChangeDependencies()” //在初始化时引用上下文 _initBloc(); } @override void didUpdateWidget(ShoppingItemWidget oldWidget) { super.didUpdateWidget(oldWidget); //因为Flutter可能决定重新组织Widgets树 //最好重新创建链接 _disposeBloc(); _initBloc(); } @override void dispose() { _disposeBloc(); super.dispose(); } //这个例程对于创建链接是可靠的 void _initBloc() { //创建ShoppingItemBloc的实例 _bloc = ShoppingItemBloc(widget.shoppingItem); //检索处理购物篮内容的BLoC _shoppingBloc = BlocProvider.of
(context); //传输购物内容的简单管道 //购物篮子到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. 这一切如何运作?

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

结论

另一篇长篇文章,我希望我能缩短一点,但我认为值得一些解释。

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

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

大多数肯定有其他方法可以做到这一点,甚至更好的方式,但它只对我有用,这就是我想与你分享的一切。

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

转载于:https://juejin.im/post/5ca3974df265da308939fa72

你可能感兴趣的文章
手机monkey测试BUG重现及解决方法
查看>>
linux安装至少有哪两个分区,各自作用是什么?
查看>>
Java的位运算符详解实例——与(&)、非(~)、或(|)、异或(^)【转】
查看>>
转载: 数据库索引原理和优缺点
查看>>
swoole 安装和简单实用
查看>>
文件系统 第八次迭代 VFS相关说明
查看>>
Java集合篇五:HashMap
查看>>
InfoPi运行机制介绍
查看>>
速读《构建之法:现代软件工程》提问
查看>>
SpringCloud注册中心环境搭建euraka
查看>>
各类文件的文件头标志
查看>>
第四周作业——在你的实际项目旅游网站中,网页主页面主要有哪些模块?
查看>>
基于django的个人博客网站建立(一)
查看>>
ElasticSearch 安装使用
查看>>
使用nodejs创建加入用户验证的websocket服务
查看>>
反思最近这些时日的荒废
查看>>
React性能分析利器来了,妈妈再也不用担心我的React应用慢了(转)
查看>>
linux学习
查看>>
[CTSC2010]珠宝商 SAM+后缀树+点分治
查看>>
[SDOI2016]储能表——数位DP
查看>>