How to Use BLoC for Flutter State Management? An In-Depth Tutorial

Biztech

By : Biztech

How to Use BLoC for Flutter State Management? An In-Depth Tutorial1

Summary

State management is crucial in Flutter app development because you don’t want your data to be scattered everywhere. With the help of state management, you will be able to control the flow of data in your application and centralize the UI state.

With Flutter development, you have several state management libraries such as BLoC (Business logic component), MobX, Cube, GetX, Inherited Widget, etc. In this blog, we will talk about one of the most popular libraries BLoC, which is recommended by Google as well.

Here, we will see in detail about state management using BLoC and how you can set it up for use.

So without further ado, let’s check!

In Flutter BLoC, everything is a widget. The widget can be classified into two categories:

I. Stateless widget

The Stateless widget does not have any internal state. It means once it is built, we cannot change or modify it until they are initialized again.

II. Stateful widget

A Stateful widget is dynamic and has a state. It means we can modify it easily throughout its lifecycle without reinitializing it.

What is State?

A state is information that can be read when the widget is built and might change or modify over the lifetime of the app. The setState() function allows us to set the properties of the state object that triggers a redraw of the UI.

What is State Management?

State management is one of the most popular and necessary processes in the lifecycle of an application. By official documentation, Flutter is declarative. It means Flutter builds its UI by reflecting the current state of your app.

To manage the large size of applications we must need to manage states. There are many state management libraries in flutter such as Provider, BLoC, Redux, GetX Etc.

In this blog, we will talk about BLoC Architecture.

What is BLoC?

flutter bloc tutorial

BLoC stands for Business Logic Components. It’s a state management system for Flutter recommended by Google developers. It helps in managing the state and makes access to data from a central place in your project.

Pros and Cons Of BLoC Pattern

Before moving forward in the Flutter BLoC, let’s examine some of the pros and cons of the bloc pattern.

Pros of BLoC

  • Easy to separate UI from logic
  • Easy to test code
  • Easy to reuse code
  • Good performance

Cons of BloC

  • The learning curve is a bit steep.
  • More boilerplate code, but that can be taken care of by extensions.
  • Not recommended for simple applications.

Read More: What’s New in Flutter 3.19 Update

Flutter BLoC Tutorial

We will build a simple application to demonstrate how BLoC uses streams to manage states and write some tests for the bloc.

We will build an authentication application; create a new user and log in with credentials. You can refer below GIF for the same.

Initial Setup

1. Create a New Flutter Project

To create a new Flutter project from the Flutter starter app template:

  • In the IDE, click New Project from the Welcome window or File > New > Project from the main IDE window.

Initial Setup 1

  • Specify the Flutter SDK path and click Next.

Initial Setup 2

  • Enter your desired Project name, Description, and Project location.

Initial Setup 3

  • Set the package name and tap on ‘Finish’.

Initial Setup 4

2. Add BLoC Package Library to pubspec.yaml

Add BLoC Package Library

It will give you the option for a generation bloc for your project.

dependencies:

flutter_bloc: ^8.1.1

flutter bloc example

3. Create all required files for BLoC (as shown in the below image)

flutter bloc example

Create the same file for the login screen also.

Understanding BLoC Concepts: Events and States

To understand how BLoC works, we need to know what are events and states.

  • Events: events are an application’s inputs (like button_press to load images, text inputs).
  • States: States are simply the application’s state, which can be changed in response to the event received.

Bloc manages these events and states.

Any Event which is performed in UI is managed in the bloc. Firstly, bloc requests data from database or backend side, and then bloc emits state according to events as Shown in the image below.

Events And States

4. Creating an Event

part of 'signup_bloc.dart';
@immutable
abstract class SignupEvent {}
//GetSignup Event for Creating New User
class GetSignup extends SignupEvent {
String? fullname;
String? email;
String? password;
String? phoneNumber;
GetSignup({this.email, this.password, this.fullname, this.phoneNumber}); }

Here we have created a GetSignup, which will be fired when a button is clicked. We have an abstract SignupEvent class because Bloc expects a single event to be added to the stream.

There can be multiple events in an app, we create an abstract class and extend it whenever we want to create any new event for handling and passing multiple events to the bloc.

5. Creating an State

part of 'signup_bloc.dart';
@immutable
abstract class SignupState {}
class SignupInitial extends SignupState {}
class SignupValidation extends SignupState {
String? value;
SignupValidation(this.value);
}
class SignupLoading extends SignupState {}
class SignupLoaded extends SignupState {}
class SignupError extends SignupState {
String? error;
SignupError(this.error);
}

Here we have 5 different states which emit one by one as defined in SignupBloc.

6. Event and State Management using BLoC Pattern

Firstly we need to add packager of Shared Preferences and import file to the SignupBloc.

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'signup_event.dart';
part 'signup_state.dart';
class SignupBloc extends Bloc<SignupEvent, SignupState> { SignupBloc() : super(SignupInitial()) {
on((event, emit) async {
if (event is GetSignup) {
String value = validation(event);
if (value != '') {
emit(SignupValidation(value));
} else {
emit(SignupLoading());
//holds the user for below given time
await Future.delayed(const Duration(seconds: 0), () async { //storing data in SharedPreferences
SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setString('email', "${event.email}");
prefs.setString('password', "${event.password}"); emit(SignupLoaded());
}).onError((error, stackTrace) {
emit(SignupError(error.toString()));
});
}
}
});
}
}
//validation for text field
String validation(GetSignup data) {
if (data.fullname?.isEmpty == true) {
return 'Please Enter Your Fullname';
}
if (data.email?.isEmpty == true) {
return 'Please Enter Your Email-id';
}
if (data.password?.isEmpty == true) {
return 'Please Enter Your Password';
}
if (data.phoneNumber?.isEmpty == true) {
return 'Please Enter Your Contact Number';
}
return '';
}

Explanation

This is the part that contains the business logic of our application.

  • Event is executed when GetSignup is added to the stream via a button click, it receives the event. (Any information that you want to pass along with triggering event you can access it using this), emit which is used to emit a state for that particular event.
  • When the BLoC is called the default initial state is emitted.
  • on < SignupEvent > invokes the BlocEvents after that our triggered event is started.
  • InitialState when our UI Screen builds and default state.
  • Here our application is an authentication app so we used Validation State.
  • It checks the validation of the screen. For example, TextField is not empty.
  • The Loading State for showing Progress Loader And API Calling From Backend Side. In this application, we are not using API but we have done it by SharedPrefrences.
  • The Loaded State when all data and processes are successfully completed if any error happens in BLoC the Error State emits.
  • The State and one by one emitted and if any problem occurs the OnError is called.

7. Providing our BLoC

Now events, states, bloc, and our application’s UI are not connected. Let’s start connecting them together.

import 'package:authentication_user/home_screen/screen/home_screen.dart'; import 'package:authentication_user/login_screen/screen/login_screen.dart'; import 'package:authentication_user/signup_screen/bloc/signup_bloc.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SignupScreen extends StatefulWidget {
@override
State createState() => _SignupScreenState(); }
class _SignupScreenState extends State {
//instance of LoginBloc
final _signupBloc = SignupBloc();
//controller for text field
TextEditingController fullNameController = TextEditingController(); TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController(); TextEditingController phoneNumberController = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocProvider(
//providing signup bloc
create: (context) {
return _signupBloc;
},
child: BlocListener<SignupBloc, SignupState>(
//providing signup bloc listener
listener: (context, state) {
if (state is SignupValidation) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("${state.value}"),
));
}
if (state is SignupLoading) {}
if (state is SignupLoaded) {
//if successfully created user then loaded state called ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Successfully Registered New User"), ));
Navigator.push(context, MaterialPageRoute( builder: (context) {
return HomeScreen(); //push to HomeScreen },
));
}
},
child: Scaffold(
appBar: AppBar(
title: Text('Authentication App'),
backgroundColor: Colors.redAccent,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center, children: [
Container(
child: Text(
'Create New User',
style: TextStyle(
fontSize: 25,
color: Colors.redAccent,
fontWeight: FontWeight.bold),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 20), child: Column(
children: [
SizedBox(
height: 20,
),
TextField(
textInputAction: TextInputAction.next, keyboardType: TextInputType.name, controller: fullNameController,
decoration: InputDecoration(
hintText: 'Full name',
// errorText: validate ? errorText : null, focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.redAccent), borderRadius: BorderRadius.circular(10)), border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black38), borderRadius: BorderRadius.circular(10)), ),
),
SizedBox(
height: 10,
),
TextField(
textInputAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, controller: emailController,
decoration: InputDecoration(
hintText: 'Email',
// errorText: validate ? errorText : null, focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.redAccent), borderRadius: BorderRadius.circular(10), ),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black38), borderRadius: BorderRadius.circular(10)), ),
),
SizedBox(
height: 10,
),
TextField(
textInputAction: TextInputAction.next, keyboardType: TextInputType.visiblePassword, controller: passwordController,
decoration: InputDecoration(
hintText: 'Password',
// errorText: validate ? errorText : null, focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.redAccent), borderRadius: BorderRadius.circular(10)),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black38), borderRadius: BorderRadius.circular(10)), ),
),
SizedBox(
height: 15,
),
TextField(
textInputAction: TextInputAction.done, keyboardType: TextInputType.phone, controller: phoneNumberController,
decoration: InputDecoration(
hintText: 'Phone number',
// errorText: validate ? errorText : null, focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.redAccent), borderRadius: BorderRadius.circular(10)),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black38), borderRadius: BorderRadius.circular(10)), )),
SizedBox(
height: 30,
),
Container(
padding: EdgeInsets.all(20),
child: Column(
children: [
Row(
children: [
Expanded(
child: ElevatedButton(
style: ButtonStyle(
elevation: MaterialStatePropertyAll(6), backgroundColor:
MaterialStatePropertyAll( Colors.redAccent),
shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius:
BorderRadius.circular(6)))), onPressed: () {
setState(() {
//adding event on widget onPressed Method _signupBloc.add(GetSignup( email: emailController.text, fullname: fullNameController.text, password: passwordController.text, phoneNumber:
phoneNumberController.text)); });
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
child: Text(
'Signup',
))),
),
],
),
SizedBox(
height: 15,
),
GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute( builder: (context) {
return LoginScreen();
},
));
},
child: Text(
'Already have an account?',
style: TextStyle(color: Colors.redAccent), ),
),
],
),
)
],
),
),
],
),
),
),
);
}
}

BlocProvider(…)

we use it to provide an instance of our bloc by placing it just below the root of the application so that it is accessible throughout it.

  • create: it creates the instance of our SignupBloc.●

BlocListner(…)

It is the portion where everything management of states happens.

  • It has a property called listener, which listens for state changes and can react in a particular way to a specific state along with state change.

Explanation:

  • When the Signup Button is fired the GetSignup Event In Bloc Is Called.
  • Bloc Provider has instance of SignupBloc and BlocListner listens the state change and gives response respect to SignupBloc.
  • First, it emits Signup Validation state where validation of the screen is managed.
  • If successfully validated, then Signup Loading state is called and in signup bloc the email and password of users are stored in local storage.
  • Signup Loaded is called if all recent state is emitted and if an error occurs then onError is invoked.

create New User

Read also: Everything about Flutter 3

Login Screen BLoC :

1. Creating an Event

part of 'login_bloc.dart';
@immutable
abstract class LoginEvent {}
//event for GetUserLogin
class GetLogin extends LoginEvent {
String? email;
String? password;
GetLogin({this.email, this.password});
}

Here we have created a GetLogin, which will be fired when a button is clicked.

2. Creating a State

part of 'login_bloc.dart';
@immutable
abstract class LoginState {}
class LoginInitial extends LoginState {}
class LoginValidation extends LoginState {
String? value;
LoginValidation(this.value);
}
class LoginLoading extends LoginState {}
class LoginLoaded extends LoginState {}
class LoginError extends LoginState {
String? error;
LoginError(this.error);
}

Here we have 5 different states which emit one by one by defined in LoginBloc.

3. Event and State Management using BLoC Pattern

Firstly we need to add packager of Shared Preferences and import file to the LoginBloc.

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; part 'login_event.dart';
part 'login_state.dart';
String? sfEmail;
String? sfPassword;
class LoginBloc extends Bloc<LoginEvent, LoginState> { LoginBloc() : super(LoginInitial()) {
//calling function to get value by SharedPreferences getStringValuesSF();
on((event, emit) async {
if (event is GetLogin) {
String value = validation(event);
if (value != '') {
emit(LoginValidation(value.toString()));
} else {
emit(LoginLoading());
//holds the user for below given time
await Future.delayed(const Duration(seconds: 3), () async { //if credentials matches then loaded state emitted if (event.email == sfEmail && event.password == sfPassword) { emit(LoginLoaded());
}
}).onError((error, stackTrace) {
emit(LoginError(error.toString()));
});
}
}
});
}
}
//validation for text fields
String validation(GetLogin data) {
if (data.email?.isEmpty == true) {
return 'Please Enter Your Email-id';
}
if (data.password?.isEmpty == true) {
return 'Please Enter Your Password';
}
if (data.email != sfEmail) {
return 'Please enter your valid email id';
}
if (data.password != sfPassword) {
return 'wrong password';
}
return '';
}
//function for getting value from SharedPreferences getStringValuesSF() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); sfEmail = prefs.getString('email').toString();
sfPassword = prefs.getString('password').toString(); }

Explanation

  • Event is executed when GetLogin is added to the stream via clicking a button , it receives the event. ( any information that you want to pass along with triggering event you can access it using this ), emit which is used to emit a state for that particular event.
  • When the BLoC is called the default initial state is emitted.
  • on < LoginEvent > invokes the BlocEvents after that our triggered event is started.
  • InitialState when our UI Screen builds and default state.
  • Firstly, we call getStringValuesSF() to get our stored value from SharedPrefrences and assign value in a global variable which is sfEmail and sfPassword.
  • Our application is authentication app so we used Validation State. It Checks Validation Of the Screen. For example, TextField is not empty.
  • The Loading State for showing Progress Loader And API Calling From Backend Side. In this application, we are not using API but we have done it by SharedPrefrences.
  • The Loaded State when all data and processes are successfully completed if any error appears in BLoC the Error State emits.
  • The State and one by one emitted and if any problem occurs the OnError is called.

4. Providing our BLoC

Now events, states, bloc, and our application’s UI are not connected. Let’s start again connecting them together.

import 'package:authentication_user/home_screen/screen/home_screen.dart'; import 'package:authentication_user/login_screen/bloc/login_bloc.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class LoginScreen extends StatefulWidget {
@override
State createState() => _LoginScreenState(); }
class _LoginScreenState extends State {
//instance of Login Bloc
final _loginBloc = LoginBloc();
//controllers for text fields
TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocProvider(
//providing login bloc
create: (context) {
return _loginBloc;
},
child: BlocListener<LoginBloc, LoginState>(
//providing listener for login bloc
listener: (context, state) {
if (state is LoginValidation) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("${state.value}"),
));
}
if (state is LoginLoading) {}
if (state is LoginLoaded) {
//if successfully credentials matches the loaded state called ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Successfully Logged in"),
));
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return HomeScreen(); //push to HomeScreen
},
));
}
},
child: Scaffold(
appBar: AppBar(
title: Text('Authentication App'),
backgroundColor: Colors.redAccent,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center, children: [
Container(
child: Text(
'Welcome Back',
style: TextStyle(
fontSize: 25,
color: Colors.redAccent,
fontWeight: FontWeight.bold),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 20), child: Column(
children: [
SizedBox(
height: 20,
),
TextField(
textInputAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, controller: emailController,
decoration: InputDecoration(
hintText: 'Email',
// errorText: validate ? errorText : null, focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.redAccent), borderRadius: BorderRadius.circular(10), ),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black38), borderRadius: BorderRadius.circular(10)), ),
),
SizedBox(
height: 10,
),
TextField(
textInputAction: TextInputAction.next, keyboardType: TextInputType.visiblePassword, controller: passwordController,
decoration: InputDecoration(
hintText: 'Password',
// errorText: validate ? errorText : null, focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.redAccent), borderRadius: BorderRadius.circular(10)),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black38), borderRadius: BorderRadius.circular(10)), ),
),
SizedBox(
height: 30,
),
Container(
padding: EdgeInsets.all(20),
child: Column(
children: [
Row(
children: [
Expanded(
child: ElevatedButton(
style: ButtonStyle(
elevation: MaterialStatePropertyAll(6), backgroundColor: MaterialStatePropertyAll( Colors.redAccent),
shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius:
BorderRadius.circular(6)))), onPressed: () {
setState(() {
//adding event on Widget OnPressed Method _loginBloc.add(GetLogin(
email: emailController.text,
password: passwordController.text, ));
});
},
child: Container(
padding: EdgeInsets.symmetric( horizontal: 20, vertical: 15),
child: Text(
'Login',
),
),
),
),
],
),
SizedBox(
height: 15,
),
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Text(
'New User? Signup Here',
style: TextStyle(color: Colors.redAccent), ),
),
],
),
)
],
),
),
],
),
),
),
);
}
}

BlocProvider(…)

we use it to provide an instance of our bloc by placing it just below the root of the application so that it is accessible throughout it.

  • create: it creates the instance of our LoginBloc.

BlocListner(…)

It is the portion where everything management of states happens.

  • It has a property called listener, which listens for state changes and can react in a particular way to a specific state along with state change.

Explanation:

  • When the Login Button is fired the GetLogin Event In Bloc Is Called.
  • Bloc Provider has instance of LoginBloc and BlocListener listens the state change and gives response respect to LoginBloc.
  • First, it get value from SharedPreferences and the it emits Login Validation state where validation of screen is managed.
  • If successfully validate then Login Loading state called and in Login bloc the email and password of user which is stored in local storage are compared with the given TextEditingController’s value by user.
  • If credentials are matched then Login Loaded is called and if error occurs then onError is invoked.

Welcome back

Conclusion

The Flutter BLoC is helpful for you to get started with state management using the BLoC pattern. From this blog, we learned about the Authentication of users in a simple way By BLoC Architecture.

Keep learning , keep coding 🙂

Get a Free Consultation

    ✓ 100% Guaranteed Security of Your Information