This article has been synchronized to personal blog, welcome to come often.

[C]Reactive Programming – stream-bloc

The original address

introduce

After introducing the concepts of BLoC, Reactive Programming and Streams, I did some introductions some time ago, although it might be interesting to share with me some of the patterns THAT I use regularly and find personally useful (at least to me). These modes save me a lot of time during development and make my code easier to read and debug.

My topic is:

  • 1.BLoC Provider and InheritedWidget
  • 2. Where to initialize BLoC?
  • 3. Event state (allows transitions based on event response state)
  • 4. Table validation (allows you to control the behavior of a form based on entries and validations)
  • 5.Part Of (Allows widgets to adjust their behavior based on their presence in the list)

The full source code can be found on GitHub.

1.BLoC Provider and InheritedWidget

I take this opportunity to introduce another version of my BlocProvider, which now relies on an InheritedWidget.

The advantage of using inheritedWidgets is that we get performance.

1.1. Previous implementation

My previous BlocProvider implementation was a regular StatefulWidget like this:

abstract class BlocBase {
  void dispose();
}

// Generic BLoC provider
class BlocProvider<T extends BlocBase> extends StatefulWidget {
  BlocProvider({
    Key key,
    @required this.child,
    @required this.bloc,
  }): super(key: key);

  final T bloc;
  final Widget child;

  @override
  _BlocProviderState<T> createState() => _BlocProviderState<T>();

  static T of<T extends BlocBase>(BuildContext context){
    final type = _typeOf<BlocProvider<T>>();
    BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
    return provider.bloc;
  }

  static Type _typeOf<T>() => T;
}

class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
  @override
  void dispose(){
    widget.bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    returnwidget.child; }}Copy the code

I use the StatefulWidget to benefit from the Dispose () method to ensure that BLoC allocated resources are released when they are no longer needed.

This is nice but not optimal from a performance standpoint.

Context. AncestorWidgetOfExactType () is a as a function of time complexity is O (n), in order to retrieve some type of ancestors, it will make up to the widget tree navigation, starting from the context, incrementing a parent at a time, until you’re done. Calls to this function are acceptable if the distance from the context to the ancestor is small (that is, the O(n) result is small), but should be avoided otherwise. This is the code for this function.

@override
Widget ancestorWidgetOfExactType(Type targetType) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element ancestor = _parent;
    while(ancestor ! =null&& ancestor.widget.runtimeType ! = targetType) ancestor = ancestor._parent;returnancestor? .widget; }Copy the code

1.2. New implementation

The new implementation relies on the StatefulWidget and incorporates the InheritedWidget:

Type _typeOf<T>() => T;

abstract class BlocBase {
  void dispose();
}

class BlocProvider<T extends BlocBase> extends StatefulWidget {
  BlocProvider({
    Key key,
    @required this.child,
    @required this.bloc,
  }): super(key: key);

  final Widget child;
  final T bloc;

  @override
  _BlocProviderState<T> createState() => _BlocProviderState<T>();

  static T of<T extends BlocBase>(BuildContext context){
    finaltype = _typeOf<_BlocProviderInherited<T>>(); _BlocProviderInherited<T> provider = context.ancestorInheritedElementForWidgetOfExactType(type)? .widget;return provider?.bloc;
  }
}

class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<T>>{
  @override
  voiddispose(){ widget.bloc? .dispose();super.dispose();
  }
  
  @override
  Widget build(BuildContext context){
    return new_BlocProviderInherited<T>( bloc: widget.bloc, child: widget.child, ); }}class _BlocProviderInherited<T> 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;
}
Copy the code

The advantage is that this solution is performance.

By using InheritedWidget, it can now call context. AncestorInheritedElementForWidgetOfExactType () function, it is a O (1), which means that the ancestors of retrieval is immediately, as shown in the source code:

@override
InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null 
                                    ? null 
                                    : _inheritedWidgets[targetType];
    return ancestor;
}
Copy the code

This comes from the fact that all InheritedWidgets are remembered by the Framework.

  • Why use ancestorInheritedElementForWidgetOfExactType?
  • You may have noticed I use ancestorInheritedElementForWidgetOfExactType method instead of the usual inheritFromWidgetOfExactType.
  • The reason is that I don’t want the Context-called BlocProvider to be registered as a dependency on the InheritedWidget, because I don’t need it.

1.3. How do I use the new BlocProvider?

1.3.1. Injection BLoC
Widget build(BuildContext context){
    returnBlocProvider<MyBloc>{ bloc: myBloc, child: ... }}Copy the code
1.3.2. Retrieval BLoC
Widget build(BuildContext context){ MyBloc myBloc = BlocProvider.of<MyBloc>(context); . }Copy the code

2. Where to initialize BLoC?

To answer this question, you need to understand its scope of use.

2.1. Available everywhere in applications

Suppose you have to deal with some of the mechanisms associated with user authentication/profiles, user preferences, and shopping baskets to get BLoC() from any possible part of the application (for example, from different pages), there are two ways to make this BLoC accessible.

2.1.1. Use global singletons
import 'package:rxdart/rxdart.dart';

class GlobalBloc {
  ///
  /// The flow associated with this BLoC
  ///
  BehaviorSubject<String> _controller = BehaviorSubject<String> ();Function(String) get push => _controller.sink.add;
  Stream<String> get stream => _controller;

  ///
  / / / factory Singleton
  ///
  static final GlobalBloc _bloc = new GlobalBloc._internal();
  factory GlobalBloc(){
    return _bloc;
  }
  GlobalBloc._internal();
  
  ///
  /// Resource disposal
  ///
  voiddispose(){ _controller? .close(); } GlobalBloc globalBloc = GlobalBloc();Copy the code

To use this BLoC, you simply import the class and call its methods directly, as follows:

import 'global_bloc.dart';

class MyWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context){
        globalBloc.push('building MyWidget');
        returnContainer(); }}Copy the code

This is an acceptable solution if you need to have a BLoC that is unique and needs to be accessed from any location within the application.

  • This is very easy to use;
  • It doesn’t depend on any BuildContext;
  • There is no need to use any BlocProvider to find BLoC;
  • To free its resources, just be sure to implement the application as a StatefulWidget and call GlobalBlock.dispose () in the override dispose () method of the application Widget

Many purists oppose this solution. I don’t know why, but… So let’s look at another……

2.1.2. Put it above everything else

In A Flutter, the ancestor of all pages must itself be the parent of the MaterialApp. This is due to the fact that a page (or route) is wrapped in an OverlayEntry, a common child stack for all pages.

In other words, each page has a Buildcontext, which is independent of any other pages. This explains why 2 pages can’t have anything in common without using any tricks.

Therefore, if you need to use BLoC anywhere in your application, it must be the parent of the MaterialApp, as shown below:

void main() => runApp(Application());

class Application extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<AuthenticationBloc>(
      bloc: AuthenticationBloc(),
      child: MaterialApp(
        title: 'BLoC Samples', theme: ThemeData( primarySwatch: Colors.blue, ), home: InitializationPage(), ), ); }}Copy the code

2.2. Can be used for subtrees

In most cases, you may need to use BLoC in some specific part of your application.

As an example, we can think of discussion topics in which groups will be used

  • Interact with the server to retrieve, add, and update posts
  • Lists the threads to display on a particular page
  • .

Therefore, if you need to use BLoC anywhere in your application, it must be the parent of the MaterialApp, as shown below:

class MyTree extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    returnBlocProvider<MyBloc>( bloc: MyBloc(), child: Column( children: <Widget>[ MyChildWidget(), ], ), ); }}class MyChildWidget extends StatelessWidget {
  @override 
  Widget build(BuildContext context){
    MyBloc = BlocProvider.of<MyBloc>(context);
    returnContainer(); }}Copy the code

This way, all widgets can access BLoC by calling the Blocprovider.of method

Ps: The solution shown above is not the best solution as it will instantiate BLoC at every reconstruction. Consequences:

  • You will lose any existing BLoC contents
  • It consumes CPU time because it needs to be instantiated on every build.

A better approach, in this case, is to use the StatefulWidget from its persistent beneficiary country, as follows:

class MyTree extends StatefulWidget {
 @override
  _MyTreeState createState() => _MyTreeState();
}
class _MyTreeState extends State<MyTree>{
  MyBloc bloc;
  
  @override
  void initState(){
    super.initState();
    bloc = MyBloc();
  }
  
  @override
  voiddispose(){ bloc? .dispose();super.dispose();
  }
  
  @override
  Widget build(BuildContext context){
    returnBlocProvider<MyBloc>( bloc: bloc, child: Column( children: <Widget>[ MyChildWidget(), ], ), ); }}Copy the code

With this approach, if the “MyTree” widget needs to be rebuilt, BLoC does not need to be re-instantiated and existing instances reused directly.

2.3. Applies to one widget only

This involves the case of BLoC being used only by one Widget.

In this case, BLoC can be instantiated in the Widget.

3. Event state (allows transitions based on event response state)

Sometimes it can become very difficult to program a series of activities that may be sequential or parallel, long or short, synchronous or asynchronous, and can lead to a variety of outcomes. You may also need to update the display as well as progress or according to status.

The first use case is designed to make this situation easier to handle.

The solution is based on the following principles:

  • Emit an event;
  • This event triggers actions that cause one or more states;
  • Each of these states can in turn emit other events or cause another state;
  • These events then trigger other actions based on the activity state;
  • And so on…

To illustrate this concept, let’s look at two common examples:

Application initialization

  • Suppose you need to run a series of operations to initialize the application. Operations may be associated with server interactions (for example, loading some data). During this initialization, you may need to display a progress bar and a series of images to keep the user waiting.

certification

  • At startup, the application may require user authentication or registration. Once the user is authenticated, it is redirected to the main page of the application. Then, if the user logs out, it is redirected to the authentication page.

To be able to handle all possible scenarios, sequences of events, but if we think we can trigger events anywhere in the application, this can become very difficult to manage.

This is BlocEventState, also BlocEventStateBuilder, which can help a lot…

3.1. BlocEventState

The idea behind BlocEventState is to define a BLoC:

  • Accept events as input;
  • Call eventHandler when a new event is emitted;
  • EventHandler is responsible for taking appropriate action based on the incident and issuing status in response.

Here’s the idea:

This is the source code for this class. The explanation is as follows:

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<BlocEvent.BlocState> implements BlocBase {
  PublishSubject<BlocEvent> _eventController = PublishSubject<BlocEvent>();
  BehaviorSubject<BlocState> _stateController = BehaviorSubject<BlocState>();

  ///
  /// call to emit events
  ///
  Function(BlocEvent) get emitEvent => _eventController.sink.add;

  ///
  /// Current/new state
  ///
  Stream<BlocState> get state => _stateController.stream;

  ///
  /// External handling of events
  ///
  Stream<BlocState> eventHandler(BlocEvent event, BlocState currentState);

  ///
  /// initialState
  ///
  final BlocState initialState;

  //
  // constructor
  //
  BlocEventStateBase({
    @required this.initialState,
  }){
    //
    // For each event received, we call [eventHandler] and emit the newState of any result
    //
    _eventController.listen((BlocEvent event){
      BlocState currentState = _stateController.value ?? initialState;
      eventHandler(event, currentState).forEach((BlocState newState){
        _stateController.sink.add(newState);
      });
    });
  }

  @override
  voiddispose() { _eventController.close(); _stateController.close(); }}Copy the code

As you can see, this is an abstract class that needs to be extended to define the behavior of the eventHandler methods.

He made a public:

  • A Sink (emitEvent) to push an event;
  • A stream (status) to listen for emission status.

At initialization (see constructor) :

An initialization state needs to be set;

  • It creates a StreamSubscription to listen to incoming events
  • Send them to eventHandler
  • Emits the result state.

3.2. Dedicated BlocEventState

The template used to implement such BlocEventState is given below. After that, we will implement the real.

class TemplateEventStateBloc extends BlocEventStateBase<BlocEvent.BlocState> {
  TemplateEventStateBloc()
      : super(
          initialState: BlocState.notInitialized(),
        );

  @override
  Stream<BlocState> eventHandler( BlocEvent event, BlocState currentState) async* {
     yieldBlocState.notInitialized(); }}Copy the code

If this template does not compile, don’t worry…… This is normal because we haven’t defined Blocstate.notinitialized ()…… This will happen in a few minutes.

This template only provides initialState and overrides eventHandler at initialization time.

There are some very interesting things to note here. We use asynchronous generators: async * and yield statements.

Use the async * modifier to mark a function as an asynchronous generator:

Each time the yield statement is called, it adds the yield output stream to the result of the following expression.

This is very useful if we need to issue a sequence of States, resulting from a sequence of actions (we’ll see later, in practice).

For additional details on asynchronous generators, click this link.

3.3. BlocEvent and BlocState

As you’ll notice, we’ve defined a BlocEvent and BlocState abstract class.

These classes need to be extended with special events and states that you want to emit.

3.4. BlocEventStateBuilder widget

The last part of the pattern is the BlocEventStateBuilder widget, which allows you to launch BlocEventState in response to State(s).

Here’s the source code for it:

typedef Widget AsyncBlocEventStateBuilder<BlocState>(BuildContext context, BlocState state);

class BlocEventStateBuilder<BlocEvent.BlocState> extends StatelessWidget {
  const BlocEventStateBuilder({
    Key key,
    @required this.builder,
    @required this.bloc,
  }): assert(builder ! =null),
      assert(bloc ! =null),
      super(key: key);

  final BlocEventStateBase<BlocEvent,BlocState> bloc;
  final AsyncBlocEventStateBuilder<BlocState> builder;

  @override
  Widget build(BuildContext context){
    return StreamBuilder<BlocState>(
      stream: bloc.state,
      initialData: bloc.initialState,
      builder: (BuildContext context, AsyncSnapshot<BlocState> snapshot){
        returnbuilder(context, snapshot.data); }); }}Copy the code

This Widget is just a specialized StreamBuilder that calls the Builder input parameter every time a new BlocState is issued.


good Now that we have all the pieces, it’s time to show what we can do with them……

3.5. Case 1: Application initialization

The first example illustrates a situation where you need the application to perform certain tasks at startup.

Common uses are for the game to initially display a splash screen (animated or not), while fetching some files from the server, checking if new updates are available, and trying to connect to any game center…… Before the actual home screen is displayed. In order not to give the application the impression that it is doing nothing, it might display a progress bar and periodically display some images while it does all the initialization.

The implementation I’m going to show you is very simple. It will only show some competitive percentages on the screen, but this can easily be extended to your needs.

3.5.1 track of. ApplicationInitializationEvent

In this example, I only consider two events:

  • Start: This event triggers the initialization process;
  • Stop: This event can be used to force the initialization process to stop.

This is the definition code implementation:

class ApplicationInitializationEvent extends BlocEvent {
  
  final ApplicationInitializationEventType type;

  ApplicationInitializationEvent({
    this.type: ApplicationInitializationEventType.start,
  }) : assert(type ! =null);
}

enum ApplicationInitializationEventType {
  start,
  stop,
}
Copy the code

3.5.2. ApplicationInitializationState

This class provides information related to the initialization process.

For this example, I would consider:

  • 2 flag: isInitialized indicates whether initialization has completed isInitializing to know if we are in the middle of the initialization process
  • Schedule completion rate

Here’s the source code for it:

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,); }}Copy the code

3.5.3. ApplicationInitializationBloc

BLoC is responsible for the event-based processing initialization process.

Here’s the code:

class ApplicationInitializationBloc
    extends BlocEventStateBase<ApplicationInitializationEvent.ApplicationInitializationState> {
  ApplicationInitializationBloc()
      : super(
          initialState: ApplicationInitializationState.notInitialized(),
        );

  @override
  Stream<ApplicationInitializationState> 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));
        yieldApplicationInitializationState.progressing(progress); }}if (event.type == ApplicationInitializationEventType.stop){
      yieldApplicationInitializationState.initialized(); }}}Copy the code

Some explanations:

  • When I received event “ApplicationInitializationEventType. Start”, it start from 0 to 100 (units of 10), and for each value (0,10,20,…) It emits (by yield) an told new state that initializes being run (isInitializing = true) and its progress value.
  • When receiving events “ApplicationInitializationEventType. Stop”, it was assumed that the initialization is complete.
  • As you can see, I put some delay in the counter loop. This will show you how to use any Future (for example, if you need to contact the server)
3.5.4. Pack them all together

Now, what’s left is a pseudo-Splash screen that displays the counter……

class InitializationPage extends StatefulWidget {
  @override
  _InitializationPageState createState() => _InitializationPageState();
}

class _InitializationPageState extends State<InitializationPage> {
  ApplicationInitializationBloc bloc;

  @override
  void initState(){
    super.initState();
    bloc = ApplicationInitializationBloc();
    bloc.emitEvent(ApplicationInitializationEvent());
  }

  @override
  voiddispose(){ bloc? .dispose();super.dispose();
  }

  @override
  Widget build(BuildContext pageContext) {
    return SafeArea(
      child: Scaffold(
        body: Container(
          child: Center(
            child: BlocEventStateBuilder<ApplicationInitializationEvent, ApplicationInitializationState>(
              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}% ');
              },
            ),
          ),
        ),
      ),
    );
  }
}
Copy the code

Description:

  • Because ApplicationInitializationBloc don’t need to use anywhere in the application, we can initialize it in StatefulWidget;
  • We directly send ApplicationInitializationEventType. Start event to trigger the eventHandler
  • Every time send ApplicationInitializationState, we will update the text
  • After initialization, we redirect the user to the home page.

Special effects

Because we cannot redirect to the home page, directly within the builder, we use WidgetsBinding. Instance. AddPostFrameCallback () method request Flutter executed immediately after the completion of the render method

3.6. Case 2: Application authentication and logout

For this example, I’ll consider the following use cases:

  • At startup, if the user is not authenticated, the “Authentication/Registration” page will be displayed automatically.
  • During the period of user authentication, show CircularProgressIndicator;
  • After authentication, the user is redirected to the home page;
  • Users can log out from anywhere in the application;
  • When a user logs out, the user is automatically redirected to the Authentication page.

Of course, it is quite possible to deal with all this programmatically, but it is much easier to delegate all this to BLoC.

The diagram below illustrates the solution I’m going to explain:

An intermediate page called “DecisionPage” will be responsible for automatically redirecting the user to the “Authenticate” page or home page, depending on the status of the user’s authentication. Of course, this DecisionPage is never displayed and should not be treated as a page.

3.6.1. AuthenticationEvent

In this example, I only consider two events:

  • Login: Emits this event when the user is correctly authenticated;
  • Logout: The event emitted when a user logs out.

The code is as follows:

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 {}
Copy the code

3.6.2. AuthenticationState This class provides information related to the authentication process.

For this example, I would consider:

  • Point 3: isAuthenticated indicates whether authentication is complete isAuthenticating to know if we are in the middle of the authentication process hasFailed means authentication hasFailed
  • The authenticated user name

Here’s the source code for it:

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,); }}Copy the code
3.6.3. AuthenticationBloc

This BLoC is responsible for handling the authentication process based on the event.

Here’s the code:

class AuthenticationBloc
    extends BlocEventStateBase<AuthenticationEvent.AuthenticationState> {
  AuthenticationBloc()
      : super(
          initialState: AuthenticationState.notAuthenticated(),
        );

  @override
  Stream<AuthenticationState> eventHandler(
      AuthenticationEvent event, AuthenticationState currentState) async* {

    if (event is AuthenticationEventLogin) {
      // Notify us that authentication is in progress
      yield AuthenticationState.authenticating();

      // Simulate a call to the authentication server
      await Future.delayed(const Duration(seconds: 2));

      // Let us know if we were successfully authenticated
      if (event.name == "failure") {yield AuthenticationState.failure();
      } else {
        yieldAuthenticationState.authenticated(event.name); }}if (event is AuthenticationEventLogout){
      yieldAuthenticationState.notAuthenticated(); }}}Copy the code

Some explanations:

  • When the event “AuthenticationEventLogin” is received, it emits (by yield) a new state indicating that authentication is running (isAuthenticating = true).
  • It then runs the authentication, and once it’s done, it issues another status to say that the authentication is complete.
  • When receiving events “AuthenticationEventLogout”, it will send a new state, tell the user not authenticated.
3.6.4 radar echoes captured. AuthenticationPage

As you will see, this page is very basic and doesn’t do much for the sake of explanation.

Here’s the code. The explanation is as follows:

class AuthenticationPage extends StatelessWidget {
  ///
  /// Prevents the use of the "back" button
  ///
  Future<bool> _onWillPopScope() async {
    return false;
  }

  @override
  Widget build(BuildContext context) {
    AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
    return WillPopScope(
      onWillPop: _onWillPopScope,
      child: SafeArea(
        child: Scaffold(
          appBar: AppBar(
            title: Text('Authentication Page'),
            leading: Container(),
          ),
          body: Container(
            child:
                BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
              bloc: bloc,
              builder: (BuildContext context, AuthenticationState state) {
                if (state.isAuthenticating) {
                  return PendingAction();
                }

                if (state.isAuthenticated){
                  return Container();
                }
                
                List<Widget> children = <Widget>[];

                // 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,
                );    
              },
            ),
          ),
        ),
      ),
    );
  }
}
Copy the code

Description:

  • Line 11: The page retrieves a reference to AuthenticationBloc
  • Line 24-70: It listens for the AuthenticationState emitted: If authentication is in progress, it will show a CircularProgressIndicator, tell the user is in some operations and prevent users visit the page (line 25-27) if verification is successful, we don’t need to display any content (line 29-31). If the user is not authenticated, two buttons are displayed to simulate successful authentication and failure. When we click one of the buttons, we issue an AuthenticationEventLogin event, along with some parameters (usually used by the authentication process) and display an error message if the authentication fails (lines 60-64)

prompt

You may have noticed that I wrapped the page in WillPopScope. The reason is that I don’t want the user to be able to use the Android’ back ‘button, so as shown in the example, authentication is a required step that prevents the user from accessing any other parts unless properly authenticated.

3.6.5. DecisionPage

As mentioned earlier, I want the application to automatically redirect to AuthenticationPage or HomePage based on the authentication status.

Here is the code for this DecisionPage, described as follows:

class DecisionPage extends StatefulWidget {
  @override
  DecisionPageState createState() {
    return newDecisionPageState(); }}class DecisionPageState extends State<DecisionPage> {
  AuthenticationState oldAuthenticationState;

  @override
  Widget build(BuildContext context) {
    AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
    return BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
      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
        // Always alert (hence "hide") after any active page.
        returnContainer(); }); }void_redirectToPage(BuildContext context, Widget page){ WidgetsBinding.instance.addPostFrameCallback((_){ MaterialPageRoute newRoute = MaterialPageRoute( builder:  (BuildContext context) => page ); Navigator.of(context).pushAndRemoveUntil(newRoute, ModalRoute.withName('/decision')); }); }}Copy the code

remind

To explain this in detail we need to go back to the way that Flutter handles Pages (= Route). To handle routing, we use the navigator, which creates a overlay layer. This overlay is a stack of overlayentries, each of them containing pages. When we push, pop, and replace pages via navigator. of, the latter updates its reconstructed overlay (hence the stack). When the stack is rebuilt, each OverlayEntry (and therefore its contents) is also rebuilt. Therefore, when we operate through navigator.of (context), all remaining pages are rebuilt!

So why did I implement it as a StatefulWidget?

In order to be able to respond to any changes in AuthenticationState, this “page” needs to persist throughout the life cycle of the application.

This means that the page will be rebuilt each time navigator.of (context) completes the operation, as indicated above.

Therefore, its BlocEventStateBuilder will also be rebuilt, calling its own builder methods.

Because this builder is responsible for redirecting the user to the page corresponding to AuthenticationState, if we redirect the user each time we rebuild the page, it will continue redirecting because of constant rebuilding.

To prevent this, we just need to remember the last AuthenticationState we acted on, and only take another action when we receive another AuthenticationState.

How does this work?

As mentioned above, BlocEventStateBuilder calls its builder each time AuthenticationState is issued.

Based on isAuthenticated, we know which page we need to redirect users to.

Special effects

Since we cannot directly from the build to redirect to another page, so we use WidgetsBinding. Instance. AddPostFrameCallback () method in the render request after the completion of the Flutter execution method

In addition, since we need to remove any existing pages before redirecting the user, we use navigator.of (context).pushandRemoveUntil (…) in addition to this DecisionPage, which we need to keep in all cases. To do that.

3.6.6 Logout To let the user log out, you can now create a “LogOutButton” and place it anywhere in the application.

  • This button only needs to send AuthenticationEventLogout () event, which will result in the following automatic chain operation: 1. It will be processed by AuthenticationBloc 2. In turn an AuthentiationState (isAuthenticated = false) 3 is emitted. This will be handled by DecisionPage via BlocEventStateBuilder 4. This redirects the user to the AuthenticationPage
3.6.7. AuthenticationBloc

Since the AuthenticationBloc needs to be provided to any page of the application, we will also inject it as a parent of the MaterialApp, as shown below

void main() => runApp(Application());

class Application extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<AuthenticationBloc>(
      bloc: AuthenticationBloc(),
      child: MaterialApp(
        title: 'BLoC Samples', theme: ThemeData( primarySwatch: Colors.blue, ), home: DecisionPage(), ), ); }}Copy the code

4. Table validation (allows you to control the behavior of a form based on entries and validations)

Another interesting use of BLoC is when you need to validate forms:

  • Validate entries related to TextField against some business rules;
  • Display validation error messages according to rules;
  • Automate accessibility of widgets based on business rules.

An example I’ll do now is the RegistrationForm, which consists of 3 TextFields (email, password, confirm password) and 1 RaisedButton to start the registration process.

The business rule I want to implement is:

  • The E-mail must be a valid E-mail address. If not, the message needs to be displayed.
  • The password must be valid (must contain at least 8 characters, with 1 uppercase, lowercase 1, Figure 1, and 1 special character). If not, the message needs to be displayed.
  • The password must meet the same authentication rules and the same password. If not, the message needs to be displayed.
  • When registering, the button may only activate all rules that are valid.

4.1. RegistrationFormBloc

BLoC is responsible for processing validation business rules, as described earlier.

The source code is as follows:

class RegistrationFormBloc extends Object with EmailValidator.PasswordValidator implements BlocBase {

  final BehaviorSubject<String> _emailController = BehaviorSubject<String> ();final BehaviorSubject<String> _passwordController = BehaviorSubject<String> ();final BehaviorSubject<String> _passwordConfirmController = BehaviorSubject<String> ();//
  // Inputs
  //
  Function(String) get onEmailChanged => _emailController.sink.add;
  Function(String) get onPasswordChanged => _passwordController.sink.add;
  Function(String) get onRetypePasswordChanged => _passwordConfirmController.sink.add;

  //
  // Validators
  //
  Stream<String> get email => _emailController.stream.transform(validateEmail);
  Stream<String> get password => _passwordController.stream.transform(validatePassword);
  Stream<String> 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<bool> get registerValid => Observable.combineLatest3(
                                      email, 
                                      password, 
                                      confirmPassword, 
                                      (e, p, c) => true
                                    );

  @override
  voiddispose() { _emailController? .close(); _passwordController? .close(); _passwordConfirmController? .close(); }}Copy the code

Let me elaborate……

  • We first initialize three BehaviorSubjects to handle Streams for each TextField of the form.
  • We expose three functions (Strings) that will be used to accept input from TextFields.
  • We have exposed three streams that TextField will use to display potential error messages generated by their respective validations
  • We expose a Stream that will be used by RaisedButton to enable/disable it based on the entire validation result.

Ok, now it’s time to dig into more details……

As you may have noticed, this type of signature is a bit special. So let’s go back.

class RegistrationFormBloc extends Object 
                           with EmailValidator.PasswordValidator 
                           implements BlocBase {... }Copy the code

The with keyword means that the class is MIXINS (a way of reusing some class code in another class), and in order to be able to use the with keyword, the class needs to extend the Object class. These mixins contain code to verify email and password, respectively.

For details mixed in I suggest you read this big article from Romain Rastel.

4.4.1. The Validator Mixins

I’m just going to explain EmailValidator, because PasswordValidator is very similar.

First, the code:

const String _kEmailRule = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$";

class EmailValidator {
  final StreamTransformer<String.String> validateEmail = 
      StreamTransformer<String.String>.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); }}); }Copy the code

This class exposes a final function (” validateEmail “), which is a StreamTransformer.

StreamTransformer is called as follows: stream.transform (StreamTransformer). StreamTransformer references its input from the Stream through the transform method. It then processes the input and reinjects the converted input into the original Stream.

4.1.2. Why stream.transform ()?

As mentioned earlier, if validation succeeds, StreamTransformer reinjects input into the Stream. Why is it useful?

Here is with Observable.com bineLatest3 () the interpretation of the relevant… This method does not emit any values until all Streams it references, at least one value.

Let’s look at the picture below to illustrate what we want to achieve.

If the user to enter the email and the latter is validated, it will flow issued by E-mail, the E-mail flow will be Observable.com bineLatest3 () an input; If the email address is invalid, an error will be added to the stream (and no value will flow out of the stream); The same applies to passwords and re-entering passwords; When all the three authentication success (means that all the three flow send out a value), Observable.com bineLatest3 () will be issued in a real thank “(e, p, c) = > true” (see line 35).

4.1.3. Verify 2 passwords

I see a lot of problems with this comparison on the Internet. There are several solutions, so let me explain two of them.

4.1.3.1. Basic Solution – No error messages

The first solution might be one of the following:

Stream<bool> get registerValid => Observable.combineLatest3(
                                      email, 
                                      password, 
                                      confirmPassword, 
                                      (e, p, c) => (0 == p.compareTo(c))
                                    );
Copy the code

This solution only validates two passwords and emits a value (= true) if they match.

As we will see shortly, the accessibility of the Register button will depend on the registerValid stream.

If two passwords do not match, the flow emits no value and the Register button remains inactive, but the user does not receive any error messages to help him understand the reason.

4.1.3.2. Solution with error messages

Another solution involves extending the confirmPassword stream processing, as shown below:

Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
    .doOnData((String c){
      // If the password is accepted (after the validation rule)
      // We need to make sure the password matches the reentered password
      if (0! = _passwordController.value.compareTo(c)){// Add an error if they do not match
        _passwordConfirmController.addError("No Match"); }});Copy the code

Once the re-entered password is validated, it is emitted by the Stream, and using doOnData, we can directly retrieve the emitted value and compare it to the value of the cipher Stream. If the two do not match, we can now send error messages.

4.2. The RegistrationForm

Now let’s explain the RegistrationForm:

class RegistrationForm extends StatefulWidget {
  @override
  _RegistrationFormState createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
  RegistrationFormBloc _registrationFormBloc;

  @override
  void initState() {
    super.initState();
    _registrationFormBloc = RegistrationFormBloc();
  }

  @override
  voiddispose() { _registrationFormBloc? .dispose();super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      child: Column(
        children: <Widget>[
          StreamBuilder<String>(
              stream: _registrationFormBloc.email,
              builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                return TextField(
                  decoration: InputDecoration(
                    labelText: 'email',
                    errorText: snapshot.error,
                  ),
                  onChanged: _registrationFormBloc.onEmailChanged,
                  keyboardType: TextInputType.emailAddress,
                );
              }),
          StreamBuilder<String>(
              stream: _registrationFormBloc.password,
              builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                return TextField(
                  decoration: InputDecoration(
                    labelText: 'password',
                    errorText: snapshot.error,
                  ),
                  obscureText: false,
                  onChanged: _registrationFormBloc.onPasswordChanged,
                );
              }),
          StreamBuilder<String>(
              stream: _registrationFormBloc.confirmPassword,
              builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                return TextField(
                  decoration: InputDecoration(
                    labelText: 'retype password',
                    errorText: snapshot.error,
                  ),
                  obscureText: false,
                  onChanged: _registrationFormBloc.onRetypePasswordChanged,
                );
              }),
          StreamBuilder<bool>(
              stream: _registrationFormBloc.registerValid,
              builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
                return RaisedButton(
                  child: Text('Register'),
                  onPressed: (snapshot.hasData && snapshot.data == true)? () {// launch the registration process
                        }
                      : null,); })],),); }}Copy the code

Description:

  • Since RegisterFormBloc is only available for this form, this is a good place to initialize it.
  • Each TextField is wrapped in the StreamBuilder so that it can respond to any results of the validation process (see errorText: snapshot.error)
  • Every time to modify the content of the TextField, we will be sent via onChanged validated input to the BLoC: _registrationFormBloc. OnEmailChanged input (email)
  • For RegisterButton, the latter is also included in the StreamBuilder.
  • If _registrationFormBloc registerValid issued a value, onPressed method will perform certain actions
  • If no value is emitted, the onPressed method is specified as NULL, which deactivates the button.

! There are no business rules in the form, which means you can change the rules without making any changes to the form, which is great!

5.Part Of (Allows widgets to adjust their behavior based on their presence in the list)

Sometimes it is interesting for a Widget to know if it is part of the set that drives its behavior.

For the final use case in this article, I’ll consider the following scenario:

Application processing projects; Users can choose what to put in the basket; An item can only be placed in the basket once; Items stored in the basket can be taken out of the basket; Once removed, it can be retrieved.

For this example, each item will display a button that will depend on the presence of an item in the basket. If it is not part of the basket, the button will allow the user to add it to the basket. If it is part of a shopping basket, the button will allow the user to take it out of the basket.

To better illustrate the “partial” pattern, I’ll consider the following architecture:

A shopping page will display a list of all possible items; Each item in the shopping page displays a button to add or remove the item, depending on its location in the basket; If an item is added to the basket in the shopping page, its buttons will be automatically updated to allow the user to remove it from the basket (and vice versa) without having to regenerate the shopping page on another page, the shopping basket, which will list all the items in the basket; You can delete any item in the basket from this page.

This name is my personal name. It’s not an official name.

As you can imagine by now, we need to consider a BLoC dedicated to processing a list of all possible items, as well as a section of the shopping basket.

The BLoC might look like this:

class ShoppingBloc implements BlocBase {
  // A list of all the items, part of the shopping basket
  Set<ShoppingItem> _shoppingBasket = Set<ShoppingItem>();

  // Stream to a list of all possible projects
  BehaviorSubject<List<ShoppingItem>> _itemsController = BehaviorSubject<List<ShoppingItem>>();
  Stream<List<ShoppingItem>> get items => _itemsController;

  // Stream to list the items in the basket
  BehaviorSubject<List<ShoppingItem>> _shoppingBasketController = BehaviorSubject<List<ShoppingItem>>(seedValue: <ShoppingItem>[]);
  Stream<List<ShoppingItem>> get shoppingBasket => _shoppingBasketController;

  @override
  voiddispose() { _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(){
    // Provide a shopping basket flow with new content
    _shoppingBasketController.sink.add(_shoppingBasket.toList());
    
    // Any other processing, such as
    // Calculate the total value of the basket
    // Number of items, part of basket......
  }

  //
  // Generate a list of shopping items
  // Normally this should come from a call to the server
  // But for this sample, we are just simulating
  //
  void _loadShoppingItems() {
    _itemsController.sink.add(List<ShoppingItem>.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)); })); }}Copy the code

The only method that might need explaining is the _postActionOnBasket () method. Each time an item is added or removed from the basket, we need to “refresh” the contents of the _shoppingBasketController Stream so that all Widgets that are listening for changes to this Stream can be notified and refreshed/rebuilt.

5.2. ShoppingPage

This page is very simple and shows only all items.

class ShoppingPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ShoppingBloc bloc = BlocProvider.of<ShoppingBloc>(context);

    return SafeArea(
        child: Scaffold(
      appBar: AppBar(
        title: Text('Shopping Page'),
        actions: <Widget>[
          ShoppingBasket(),
        ],
      ),
      body: Container(
        child: StreamBuilder<List<ShoppingItem>>(
          stream: bloc.items,
          builder: (BuildContext context,
              AsyncSnapshot<List<ShoppingItem>> 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) {
                returnShoppingItemWidget( shoppingItem: snapshot.data[index], ); }); },),))); }}Copy the code

Description:

  • The AppBar display button, : displays the number of items that appear in the ShoppingBasket when clicked redirects the user to the ShoppingBasket page
  • The List of projects is built using GridView and included in StreamBuilder
  • Each item corresponds to a ShoppingItemWidget

5.3. ShoppingBasketPage

This page is very similar to ShoppingPage, except that the StreamBuilder is now listening for a variant of the _shoppingBasket stream exposed by ShoppingBloc.

5.4. ShoppingItemWidget and ShoppingItemBloc

The Part Of pattern relies on a combination Of these two elements

  • The ShoppingItemWidget is responsible for displaying items and buttons to add or remove items from the shopping basket
  • The ShoppingItemBloc is responsible for telling the ShoppingItemWidget whether the latter is part of the shopping basket or not. Let’s see how they work together……
5.4.1. ShoppingItemBloc

The ShoppingItemBloc is instantiated by each ShoppingItemWidget, giving it “identity”

This BLoC listens for all variants of the ShoppingBasket flow and checks if a particular item identifier is part of the basket.

If it is, it emits a Boolean value (= true), which is captured by the ShoppingItemWidget to determine if it is part of the basket.

Here’s the code for BLoC:

class ShoppingItemBloc implements BlocBase {
   // Stream, notifying if the ShoppingItemWidget is part of the shopping basket
  BehaviorSubject<bool> _isInShoppingBasketController = BehaviorSubject<bool> (); Stream<bool> get isInShoppingBasket => _isInShoppingBasketController;

  // Receive a stream of all items listed as part of the shopping basket
  PublishSubject<List<ShoppingItem>> _shoppingBasketController = PublishSubject<List<ShoppingItem>>();
  Function(List<ShoppingItem>) get shoppingBasket => _shoppingBasketController.sink.add;

   // constructor for "token" with shoppingItem
  ShoppingItemBloc(ShoppingItem shoppingItem){
    // Each time the contents of the shopping basket change
    _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
  voiddispose() { _isInShoppingBasketController? .close(); _shoppingBasketController? .close(); }}Copy the code
5.4.2. ShoppingItemWidget

This Widget is responsible for:

  • Create an instance of ShoppingItemBloc and pass its own identity to BLoC
  • Listen for any changes in ShoppingBasket content and move it to BLoC
  • Listen for the ShoppingItemBloc to see if it’s part of the basket
  • Displays the corresponding button (add/remove), depending on its presence in the basket
  • User actions that respond to the button add themselves to the basket when the user clicks on the Add button and remove themselves from the basket when the user clicks on the Delete button.

Let’s see how it works (explained in the code).

class ShoppingItemWidget extends StatefulWidget {
  ShoppingItemWidget({
    Key key,
    @required this.shoppingItem,
  }) : super(key: key);

  final ShoppingItem shoppingItem;

  @override
  _ShoppingItemWidgetState createState() => _ShoppingItemWidgetState();
}

class _ShoppingItemWidgetState extends State<ShoppingItemWidget> {
  StreamSubscription _subscription;
  ShoppingItemBloc _bloc;
  ShoppingBloc _shoppingBloc;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    // Since context should not be used in the "initState ()" method,
    // Prefers "didChangeDependencies ()" when needed
    // Reference the context during initialization
    _initBloc();
  }

  @override
  void didUpdateWidget(ShoppingItemWidget oldWidget) {
    super.didUpdateWidget(oldWidget);

    // Because Flutter may decide to reorganize the Widgets tree
    // It is best to re-create the link
    _disposeBloc();
    _initBloc();
  }

  @override
  void dispose() {
    _disposeBloc();
    super.dispose();
  }

   // This routine is reliable for creating links
  void _initBloc() {
    // Create an instance of ShoppingItemBloc
    _bloc = ShoppingItemBloc(widget.shoppingItem);

    // Retrieve BLoC for processing basket contents
    _shoppingBloc = BlocProvider.of<ShoppingBloc>(context);

     // A simple channel to transport shopping content
    // Shopping basket to ShoppingItemBloc
    _subscription = _shoppingBloc.shoppingBasket.listen(_bloc.shoppingBasket);
  }

  void_disposeBloc() { _subscription? .cancel(); _bloc? .dispose(); } Widget _buildButton() {return StreamBuilder<bool>(
      stream: _bloc.isInShoppingBasket,
      initialData: false,
      builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
        returnsnapshot.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}Euro '), ), child: Container( color: widget.shoppingItem.color, child: Center( child: _buildButton(), ), ), ), ); }}Copy the code

5.5. How does it all work?

The following figure shows how all the pieces work together.

conclusion

Another long article, I wish I could shorten it a little bit, but I think it deserves some explanation.

As I mentioned in the introduction, I personally use these “patterns” a lot in my development. This saved me a lot of time and energy; My code is easier to read and debug.

In addition, it helps separate the business from the view.

Most certainly have other ways to do this, or even better ways, but it only works for me, and that’s all I want to share with you.

Stay tuned for new articles, and have fun programming.