DEV Community

Cover image for BLoC 💪 + flutter ❤️
Prakash S
Prakash S

Posted on

BLoC 💪 + flutter ❤️

BLOC means Business Logic Of Components.
In Simple, this is kind of pattern to isolate the business logic and UI to meet the below things
1) Fast development
2) Easily testable
3) Reusable

In simple way to understand bloc, It takes an input of events and emitting an output of states
Events can be triggered mostly by UI like button tap, scrolling change, Text change etc..
States will be listened in UI to re-render the widgets

Thanks to Flutter Bloc package which comes to save us 🤩🤩.
I would recommend to go through the official documentation of flutter_bloc to get more info 😇.

We will see a real time scenario of user registration with bloc pattern in flutter

Assume we have a registration form, where we are having fields like Username, Password, Confirm password along with it we have register button.
Our model class will look like as below,

RegistrationModel

import 'package:flutter/material.dart';

class RegistrationModel {
  final String userName;
  final String password;
  final String confirmPassword;
  final bool isValidForRegistration;

  factory RegistrationModel.empty() {
    return RegistrationModel("", "", "", false);
  }

  RegistrationModel(this.userName, this.password, this.confirmPassword, this.isValidForRegistration);

  RegistrationModel copyWith(
      {String userName, String password, String confirmPassword, @required isValid}) {
    return RegistrationModel(userName ?? this.userName,
        password ?? this.password, confirmPassword ?? this.confirmPassword, isValid);
  }
}

Enter fullscreen mode Exit fullscreen mode

For the registration form, we may have following business logic
1) Username should be mandatory, length should be greater than 8 and it should not be available in the existing user repository.

2) Password should match 8 character which should contain 1 uppercase, 1 lowercase, 1 number and 1 symbol

3) Confirm password should match with the original password

If all the above condition satisfies, we will enable 👍 the button until then we will disable the register button.

For our scenario - we are going to create a RegistrationBloc to handle business logic, RegistrationEvent to feed input to the bloc & RegistrationState to emit states from Bloc to re-render the UI.

Creating a bloc is very simple, only thing you need to understand your needs upfront to coding.
The scenario for us is pretty clear, so we will jump into the code.

RegistrationState 🤠

We will have base state called RegistrationState which will take RegistrationModel as a parameter and this will be inherited by below two states

1) Initial State
2) Registration Model Changed

In Initial state, we will emit our initial model (empty model)
In Registration Model Changed, we will emit our model which has current values

part of 'registration_bloc.dart';

abstract class RegistrationState extends Equatable {
  final RegistrationModel model;
  const RegistrationState(this.model);
}

class RegistrationInitial extends RegistrationState {
  RegistrationInitial() : super(RegistrationModel.empty());

  @override
  List<Object> get props => [model];
}

class RegistrationModelChanged extends RegistrationState {
  final RegistrationModel model;
  RegistrationModelChanged(this.model) : super(model);

  @override
  List<Object> get props => [model];
}

Enter fullscreen mode Exit fullscreen mode

Registration Event 👻

As per our scenario, we need to listen 3 text field changes
So our events will be,
1) UsernameChanged
2) PasswordChanged
3) ConfirmPasswordChanged

part of 'registration_bloc.dart';

abstract class RegistrationEvent extends Equatable {
  const RegistrationEvent();
}

class UserNameChanged extends RegistrationEvent {
  final String userName;
  UserNameChanged(this.userName);
  @override
  List<Object> get props => [userName];
}

class PasswordChanged extends RegistrationEvent{
  final String password;
  PasswordChanged(this.password);

  @override
  List<Object> get props => [password];
}

class ConfirmPasswordChanged extends RegistrationEvent{
  final String confirmPassword;
  ConfirmPasswordChanged(this.confirmPassword);

  @override
  List<Object> get props => [confirmPassword];
}
Enter fullscreen mode Exit fullscreen mode

Registration bloc 🥴

Create a bloc class need three pre-requisites
1) Class should be inherited from Bloc which is from flutter_bloc package

class RegistrationBloc extends Bloc<RegistrationEvent, RegistrationState>
Enter fullscreen mode Exit fullscreen mode

2) Class should override InitialState

@override
  RegistrationState get initialState => RegistrationInitial();
Enter fullscreen mode Exit fullscreen mode

2) Class should override mapEventToState

@override
  Stream<RegistrationState> mapEventToState(
    RegistrationEvent event,
  ) async* {}
Enter fullscreen mode Exit fullscreen mode

mapEventToState is corresponds to listening the event and emitting the states
Most of our logic going to be defined here only

we are validating the model using the method IsValid


  Future<bool> isValid(
      String userName, String password, String confirmPassword) async {
    bool isValidUser = await _userRepository.isUserAvailable(userName); //username is available to create

    RegExp exp = new RegExp(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})");

    bool isUserNameValid = userName.length >= 8; // user name should have more than or equal to 8 characters
    bool isValidPassword = exp.hasMatch(password); // password should have one small case, one upper case, one number and one symbol
    bool isConfirmPasswordMatched = password == confirmPassword; // confirm password should match with the above password

    return isValidUser &&
        isUserNameValid &&
        isValidPassword &&
        isConfirmPasswordMatched; // true if all the conditions are true, else false
  }
Enter fullscreen mode Exit fullscreen mode

Wrapping up all of these,

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:bloc_tuto/registration/bloc/user_repository.dart';
import 'package:bloc_tuto/registration/model/registration_model.dart';
import 'package:equatable/equatable.dart';

part 'registration_event.dart';
part 'registration_state.dart';

class RegistrationBloc extends Bloc<RegistrationEvent, RegistrationState> {
  final UserRepository _userRepository;
  RegistrationBloc(this._userRepository);

  @override
  RegistrationState get initialState => RegistrationInitial();

  @override
  Stream<RegistrationState> mapEventToState(
    RegistrationEvent event,
  ) async* {
    if (event is UserNameChanged) {
      bool isValidModel = await isValid(
          event.userName, state.model.password, state.model.confirmPassword);
      yield RegistrationModelChanged(state.model
          .copyWith(userName: event.userName, isValid: isValidModel));
    }
    if (event is PasswordChanged) {
      bool isValidModel = await isValid(
          state.model.userName, event.password, state.model.confirmPassword);
      yield RegistrationModelChanged(state.model
          .copyWith(password: event.password, isValid: isValidModel));
    }
    if (event is ConfirmPasswordChanged) {
      bool isValidModel = await isValid(
          state.model.userName, state.model.password, event.confirmPassword);
      yield RegistrationModelChanged(state.model.copyWith(
          confirmPassword: event.confirmPassword, isValid: isValidModel));
    }
  }

  Future<bool> isValid(
      String userName, String password, String confirmPassword) async {
    bool isValidUser = await _userRepository.isUserAvailable(userName); //username is available to create

    RegExp exp = new RegExp(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})");

    bool isUserNameValid = userName.length >= 8; // user name should have more than or equal to 8 characters
    bool isValidPassword = exp.hasMatch(password); // password should have one small case, one upper case, one number and one symbol
    bool isConfirmPasswordMatched = password == confirmPassword; // confirm password should match with the above password

    return isValidUser &&
        isUserNameValid &&
        isValidPassword &&
        isConfirmPasswordMatched; // true if all the conditions are true, else false
  }
}
Enter fullscreen mode Exit fullscreen mode

UI

We are going to use 3 Textfield widgets for Username, Password & Confirm Password and BlocBuilder widget which has RaisedButton as a child.

BlocBuilder is a widget available in flutter_bloc package, which rebuilds on each state transitions.

We will listen each of the text changed and adds an event to bloc, Bloc in turns emits the necessary state which will be listened by bloc builder
So UI is now entirely isolated from our business logic, It just need to react for each state.

import 'package:bloc_tuto/registration/bloc/registration_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class RegistrationPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc pattern'),
      ),
      body: Container(
        child: Column(
          children: <Widget>[
            buildTextField(
                'UserName',
                false,
                (val) =>
                    context.bloc<RegistrationBloc>().add(UserNameChanged(val))),
            buildTextField(
                'Password',
                true,
                (val) =>
                    context.bloc<RegistrationBloc>().add(PasswordChanged(val))),
            buildTextField(
                'Confirm Password',
                true,
                (val) => context
                    .bloc<RegistrationBloc>()
                    .add(ConfirmPasswordChanged(val))),
            BlocBuilder<RegistrationBloc, RegistrationState>(
              condition: (oldState, newState) =>
                  oldState.model.isValidForRegistration !=
                  newState.model.isValidForRegistration, // re-build only when isValid changed from oldstate.
              builder: (context, state) {
                return RaisedButton(
                  onPressed: state != null && state.model.isValidForRegistration
                      ? () {} // enable press call only when model is valid for registration
                      : null,
                  child: Text('Register'),
                );
              },
            )
          ],
        ),
      ),
    );
  }

  TextField buildTextField(
      String hintText, bool isObscure, ValueChanged<String> onChangedCallback) {
    return TextField(
      obscureText: isObscure,
      decoration: InputDecoration(hintText: hintText),
      onChanged: onChangedCallback,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

As our business logic & UI was isolated each other, we can test each of them individually without affecting other.

Full Sample along with test cases see GitHub 😇😇

Happy fluttering!! 🤩🤩

Top comments (0)