BLoC 💪 + flutter ❤️
Prakash S
Posted on May 22, 2020
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);
}
}
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];
}
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];
}
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>
2) Class should override InitialState
@override
RegistrationState get initialState => RegistrationInitial();
2) Class should override mapEventToState
@override
Stream<RegistrationState> mapEventToState(
RegistrationEvent event,
) async* {}
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
}
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
}
}
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,
);
}
}
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!! 🤩🤩
Posted on May 22, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.