Flutter: Bloc Mystery | Part 1

Jitesh Mohite
FlutterWorld
Published in
5 min readMay 29, 2021

As the title suggests we are going to learn some exciting stuff about Bloc and its usage. Also will cover some of the advanced concepts which can be integrated into code easily, so keep reading….

Flutter is designed in a reactive way with asynchronous data Streams. A Stream is basically a simple object which emits data whenever any change happens. To get notified about data changes we have to subscribe to the stream, which will notify the changes until it despose.

It is like an asynchronous Iterable — where, instead of getting the next event when you ask for it, the stream tells you that there is an event when it is ready.Note: Future emits the data only once, where as Stream emit the data continuoysly or iteratavely.

This is important to have some knowledge about the stream before jumping to Bloc, as Bloc is built around the streams.

Stream uses [StreamSubscription] which listen events from this stream using the provided [onData], [onError] and [onDone] handlers.

StreamSubscription<T> listen(void onData(T event)?,
{Function? onError, void onDone()?, bool? cancelOnError});

Let's look at Example: Below example will perform addition of 1 to 10 numbers

import 'dart:async';

Stream<int> iterateSumUsingStream(int num) async* {
int sum = 0;
for (int i = 1; i <= num; i++) {
sum = sum + i;
yield sum;
}
}

void main() {
var stream = iterateSumUsingStream(10);
stream.listen(
(data) {
print('Data: $data');
},
onError: (err) {
print('Error: ${err}');
},
cancelOnError: false,
onDone: () {
print('Done!');
},
);
}
Output:Data: 1
Data: 3
Data: 6
Data: 10
Data: 15
Data: 21
Data: 28
Data: 36
Data: 45
Data: 55
Done!

As you can see, the first function onData is called 10 times, gives us the emitted value. once the for loop finished with its job then stream returns a callback in onDone

What is yield and *async? yield adds a value to the output stream of the surrounding async* function. It's like returning value but doesn't terminate the function.

So now, we have a fair idea about how the stream works, let's move towards Bloc usage.

Bloc internally comes with good architecture which we can be used in small to large-scale projects.

It has three layers in its architecture

  1. Presentation Layer
  2. Domain/Business Layer
  3. Data Layer(It consist of Repository and Provider)

Now, we will look into one simple example which will demonstrate the above layers and their usage. Here we are going to fetch user details from API and will display them on UI.

Data Layer:

The data layer can be divided into two parts

  1. Data Provider
  2. Repository

Data Provider:

The main purpose of Provider classes is to perform CRUD operations on the network or Database. We can have methods like createUsers, readUsers, updateUsers, and deleteUsers method as part of our data layer.

class UserDetailsProvider {
final String URL =
"https://5f383e6541c94900169bfd42.mockapi.io/api/v1/user_details";

/// Fetch the user details from given public URL
Future<UserDetails> fetchUserDetails() async {
final response = await http.get(
Uri.parse(URL),
);

if (response.statusCode == 200) {
print("Success");
final resData = UserDetails.fromJson(jsonDecode(response.body));
return resData;
}
throw Exception('Not able to fetch the data: ' + response.body);
}
}

Repository:

The repository contains one or more data providers, it basically communicates Between Bloc and Data Provider.

We can perform the following kind of operation

  1. Differentiate whether to call Database Provider or Network Provider.
  2. Filtering data.
  3. Joined results using different providers.
class UserDetailsRepository {
final UserDetailsProvider _userDetailsProvider = UserDetailsProvider();

// Fetch the api response and pass it to bloc component
Future<UserDetails> fetchUserDetails() async =>
_userDetailsProvider.fetchUserDetails();
}

Domain/Business Layer:

The Domain Layer has the responsibility to communicate between the presentation layer and data layer, It is used to respond to input from the presentation layer with new states. This layer can depend on one or more repositories to retrieve data needed to build up the application state.

Bloc containes state and event where Event contains the information which passed by presentation layer to make queries to database/network operation, where as State tells us about current flow of bloc, an Event could be as Loading, Loaded, Error….

Event class:

@immutable
abstract class UserDetailsEvent {}

class UserDetailsInfo extends UserDetailsEvent {}

State class:

@immutable
abstract class UserDetailsState {}

class UserDetailsLoading extends UserDetailsState {
@override
String toString() {
return "UserDetailsLoading";
}
}

class UserDetailsLoaded extends UserDetailsState {
final UserDetails userDetails;

UserDetailsLoaded(this.userDetails);

@override
String toString() {
return "UserDetailsLoaded";
}
}

class UserDetailsError extends UserDetailsState {
@override
String toString() {
return "UserDetailsError";
}
}

Bloc class:

class UserDetailsBloc extends Bloc<UserDetailsEvent, UserDetailsState> {
final UserDetailsRepository userDetailsRepository;

UserDetailsBloc({required this.userDetailsRepository})
: super(UserDetailsLoading());

@override
Stream<UserDetailsState> mapEventToState(
UserDetailsEvent event,
) async* {
yield UserDetailsLoading();
try {
UserDetails userDetails = await userDetailsRepository.fetchUserDetails();
yield UserDetailsLoaded(userDetails);
} catch (e) {
print("Exception while fetching user details: " + e.toString());
yield UserDetailsError();
}
}
}

UserDetailsBloc contains the logic to trigger a different state of the bloc to the presentation layer so that it can act based on the coming state.

Presentation Layer:

The Presentation Layer will be responsible for showing different UI based on the coming state which will be reflected in BlocBuilder. One widget can contain multiple blocs which performed asynchronously.

BlocBuilder: It must contain the required bloc and builder function, BlocBuilder handles building the widget in response to new states.

class UserDetailsScreen extends StatefulWidget {
@override
_UserDetailsScreenState createState() => _UserDetailsScreenState();
}

class _UserDetailsScreenState extends State<UserDetailsScreen> {
late final UserDetailsRepository _userDetailsRepository;
late final UserDetailsBloc _userDetailsBloc;

@override
void initState() {
_userDetailsRepository = UserDetailsRepository();
_userDetailsBloc =
UserDetailsBloc(userDetailsRepository: _userDetailsRepository);
_userDetailsBloc.add(UserDetailsInfo());
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My App'),
),
body: Center(
child: BlocBuilder(
bloc: _userDetailsBloc,
builder: (BuildContext context, UserDetailsState state) {
if (state is UserDetailsLoading) {
return CircularProgressIndicator();
}
if (state is UserDetailsLoaded) {
return UserDetailsWidget(userDetails: state.userDetails);
}
return Text('Unable to fetch the user details!!!');
}),
),
);
}
}

As you can see UserDetailsLoading, UserDetailsLoaded and UserDetailsError are returning/rendering different widgets as per the state.

UserDetailsWidget:

class UserDetailsWidget extends StatelessWidget {
final UserDetails userDetails;

const UserDetailsWidget({Key? key, required this.userDetails})
: super(key: key);

@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
),
child: Image.network(
userDetails.image ?? "",
height: 250,
width: double.infinity,
fit: BoxFit.cover,
),
),
SizedBox(height: 16.0,),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text("Name: ${userDetails.name ?? ""}"),
Text("Percentage: ${userDetails.winningPercentage ?? ""}"),
Text("Won: ${userDetails.won ?? ""}"),
],
)
],
);
}
}

Github Project: https://github.com/jitsm555/flutter_bloc_sample

Output:

--

--

Jitesh Mohite
FlutterWorld

I am technology enthusiastic, want to learn things quickly and dive deep inside it. I always believe in developing logical things which makes impact on end user