Flutter Bloc

Abubakar Saddique
31 min readJan 28, 2024

--

What is Flutter Bloc ?? :

Bloc is the global state management technique .

A predictable state management library for Dart.

The goal of this library is to make it easy to separate presentation from business logic, facilitating testability and reusability.

Bloc makes it easy to separate presentation from business logic, making your code fast, easy to test, and reusable.

The bloc is completely based on the concept of the stream’s .

Stream :

A stream is a sequence of asynchronous data.

If you want’s a detailed information about the stream’s kindly read it from the official documentation .

If you’re unfamiliar with Streams just think of a pipe with water flowing through it. The pipe is the Stream and the water is the asynchronous data.

So let’s deep dive into bloc .

A Bloc is a more advanced class which relies on events to trigger state changes rather than functions. Bloc also extends BlocBase which means it has a similar public API as Cubit. However, rather than calling a function on a Bloc and directly emitting a new state, Blocs receive events and convert the incoming events into outgoing states.

To describe the above diagram let’s take an example the counter app in which both text and the button’s are the part of the ui and on the click event of the button the text on the screen will be updated . So in this case the event of click on the button is an event that is send to the bloc . Here bloc act as the middle layer . It request’s the data or the variable of the counter for update it’s value . The data will send the response that the value will be update or not . Now bloc will send back the state either the updated value state or the error state to the ui . In the case of the updated state the value of the counter in the ui will be updated .

Now tou have notice one things that we discussed about the state and the event’s . So let’s talk a brief note here and in detail’s it will be discussed later .

Event :

Events are the input to a Bloc. They are commonly added in response to user interactions such as button presses or lifecycle events like page loads.

Let’s take’s an example of the counter app . In this example there are two event’s One is increment event on which there is one increment in the value of the counter variable and second event is decrement event in which there is one decrement in counter decrement event .

State :

State is information that can be read synchronously when the widget is built and might change during the lifetime of the widget .

States represent the various conditions that the application can be in. or in other words they are the response that provided in the response of the event’s .

Let’s take an example of the data fetched from the api .

There are four possible state’s while fetching the data .

Initial State when there is nothing and in this state the user will click’s on the button to fetch the data . This state is when the user is not yet click on the button or not yet trying to fetch the data .

Loading state when the user already click’s on the button for fetching the data and this state we will show the circular progress indicator or the loading widget .

Loaded state when the data is fetched successfully and we have to display it on the screen .

Error State is the forth and the last possible state’s in which we have to show the error on the screen when there is some thing wrong while fetching the data from the api’s like network error or other problem’s .

Now let’s start the coding for the bloc .

Before adding the package inside our project let’s me explain how the flutter bloc work’s by it’s vanilla implementation which mean’s that we use the bloc or explain how it’s work without using the bloc package .

Now let’s take an example of the counter app for the vanilla implementation .

Counter App :

Counter State :

Here we have to make all of the possible states .

Make a state class in which you declare the variable that is going to be changed and then make the const constructor of that specific state class .

 class CounterState {
final int counter;
const CounterState({required this.counter});
}

Make the counter variable final .

Counter Event :

Here we have to make the all of the possible event’s .

Make an abstract class that have all of the possible event’s like in our case we have only two event’s of increment and decrement in counter

Must add the annotation of immutable with all of the possible event’s .

Make an abstract class of the name counter event that will becomes the parent class of both of the counter increment event and counter decrement event classes .

 @immutable
abstract class CounterEvents {}


@immutable
class CounterIncrementEvent extends CounterEvents {}

@immutable
class CounterDecrementEvent extends CounterEvents {}

That’s all for the counter event and the state . Now we have to make the bloc class that act’ as the middle layer or work’s as an adapter that manage both of the event’s and the states .

Counter Bloc :

Now make a block class that can manage or handle all of the functionalities .

import 'dart:async';

import 'package:flutter_bloc_practice_f12/block_vanilla_implementation/events/counter_events.dart';
import 'package:flutter_bloc_practice_f12/block_vanilla_implementation/states/counter_state.dart';

class CounterBloc {

}

Makes the constructor of the state class and initialize the initial value to it .

CounterState _counterState = const CounterState(counter: 0);

Bloc is based on event driven programming and event can be used at any time . User can click on button anytime when it want’s so here we use the stream’s and bloc is based on the streams .

Now make the controller’s for both of the state and the event’s .

  final _counterStateStreamController = StreamController<CounterState>();
final _counterEventStreamController = StreamController<CounterEvents>();

Stream Controller :

A controller with the stream it controls.

This controller allows sending data, error and done events on its stream. and this class can be used to create a simple stream that others can listen on, and to push events to that stream.

It’s possible to check whether the stream is paused or not, and whether it has subscribers or not, as well as getting a callback when either of these change.

A controller with a [stream] that supports only one single subscriber.

StreamController<CounterState> StreamController({
void Function()? onListen,
void Function()? onPause,
void Function()? onResume,
FutureOr<void> Function()? onCancel,
bool sync = false,
})

Now here we have to make the getter of the stream of the counter state which will emit the state from the counter states using the stream (getter) of the stream controller .

  Stream<T> get stream;

The stream that this controller is controlling .

Stream<CounterState> get counterStreamState =>
_counterStateStreamController.stream;

Now we have to sink all of the event’s in the stream that will emit the state when the user click’s such as increment and decrement event .

Now here we have to make the getter of the stream sink of the counter event which will emit the state from the counter states using the stream (getter) of the stream controller .

 StreamSink<CounterEvents> get counterEventSink =>
_counterEventStreamController.sink;

The sink method of the stream controller which returns a view of this object that only exposes the [StreamSink] interface.

Stream Sink :

A object that accepts stream events both synchronously and asynchronously.

A [StreamSink] combines the methods from [StreamConsumer] and [EventSink].

abstract interface class StreamSink<S> implements EventSink<S>, StreamConsumer<S>

Now we have to make the constructor of the bloc in which we have to listen to the stream that is produced .

CounterBloc() {
_counterEventStreamController.stream.listen(mapEventToState);
}

The stream listener of the stream controller is used to adds a subscription to this stream.

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

The on data method will gives us the event’s in which we have to check the event and emit the corresponding state’s .

For that purpose i make’s a function of the name map that is placed at the position of the on data function of the listen method .

Now we have to check the event’s and update the state based on the event’s and then add the updated state in the and then sink it inside the stream .

Now we have two only two possible event’s counter increment event and counter decrement event .

Now check that if the event is counter increment event . Now we have to call the constructor of the counter state and add one in the counter variable of the counter state and assign this updated state to the counter state variable that we have declared above .

Now after we assigned the updated state to the counter state variable we have to sink the updated state in the stream controller of the counter state .

Same as in the case of the counter decrement event . We have to call the constructor of the counter state and minus one in the counter variable of the counter state and assign this updated state to the counter state variable that we have declared above .

Now after we assigned the updated state to the counter state variable we have to sink the updated state in the stream controller of the counter state .

 mapEventToState(event) {

if (event is CounterIncrementEvent) {
_counterState = CounterState(counter: _counterState.counter + 1);
_counterStateStreamController.sink.add(_counterState);
} else if (event is CounterDecrementEvent) {
_counterState = CounterState(counter: _counterState.counter - 1);
_counterStateStreamController.sink.add(_counterState);
}
}
}

That’s all for the counter bloc class . The complete code of this class is given below .

// Now make a block class that can manage or handle all of the functionalities .

import 'dart:async';

import 'package:flutter_bloc_practice_f12/block_vanilla_implementation/events/counter_events.dart';
import 'package:flutter_bloc_practice_f12/block_vanilla_implementation/states/counter_state.dart';

class CounterBloc {
// makes the constructor of the state class and initialize the initial value to it .
CounterState _counterState = const CounterState(counter: 0);

// Bloc is based on event driven and event can be used at any time . User can click on button any want's when it want's so here we use the stream's and bloc is based on the streams .

// Now make the controller's for both of the state and the event's .

final _counterStateStreamController = StreamController<CounterState>();
final _counterEventStreamController = StreamController<CounterEvents>();

// Now here we have to make the stream for both of the event's and the state .

// Now we have the stream of the
// A source of asynchronous data events.

// A Stream provides a way to receive a sequence of events. Each event is either a data event, also called an element of the stream, or an error event, which is a notification that something has failed. When a stream has emitted all its events, a single "done" event notifies the listener that the end has been reached.
Stream<CounterState> get counterStreamState =>
_counterStateStreamController.stream;

// Now we have to sink all of the event's in the stream when the user click's such as increment and decrement event .
StreamSink<CounterEvents> get counterEventSink =>
_counterEventStreamController.sink;

CounterBloc() {
// Now in the constructor of the bloc we have to listen to the that is produced .
_counterEventStreamController.stream.listen(mapEventToState);
}

mapEventToState(event) {
// Now we have to check the event's and update the state based on the event's and then add the updated state in the and then sink it inside the stream .

if (event is CounterIncrementEvent) {
_counterState = CounterState(counter: _counterState.counter + 1);
_counterStateStreamController.sink.add(_counterState);
} else if (event is CounterDecrementEvent) {
_counterState = CounterState(counter: _counterState.counter - 1);
_counterStateStreamController.sink.add(_counterState);
}
}
}

Now we have to make the ui of the counter app in which we will consume the counter bloc that we have created above .

For that purpose i make a state full widget of the name counter bloc vanilla implementation design page .


class CounterBlockVanillaImplementationDesignPage extends StatefulWidget {
const CounterBlockVanillaImplementationDesignPage({super.key, required this.title});

final String title;

@override
State<CounterBlockVanillaImplementationDesignPage> createState() => _CounterBlockVanillaImplementationDesignPageState();
}

class _CounterBlockVanillaImplementationDesignPageState extends State<CounterBlockVanillaImplementationDesignPage> {


void _incrementCounter() {

}

void _decrementCounter() {

}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text('0',);
],
),
),
floatingActionButton: ButtonBar(
children: [
FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
const SizedBox(
width: 50,
),
FloatingActionButton(
onPressed: _decrementCounter,
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
],
));
}
}

Now we have to add the following line’s to use the block vanilla implementation .

Make the final constructor of the counter block class .

final CounterBloc _counterBloc = CounterBloc();

Now we have to write code for the increment and decrement of the value of the counter in which we have simply add the event’s that we have declared like counter increment event and counter decrement event respectively in the counter event sink (getter used for the stream sink) inside the counter bloc class .

void _incrementCounter() {
_counterBloc.counterEventSink.add(CounterIncrementEvent());
}

void _decrementCounter() {
_counterBloc.counterEventSink.add(CounterDecrementEvent());
}

That’s all for these counter increment and the decrement operation’s .

Now it’s time to display the text on the screen for that purpose we need a widget of the name stream builder that will build the given child widget every time when the stream changes or update in which we have to provide the counter stream state (getter of the stream of the state’s) that we have created in the counter bloc class .

 StreamBuilder(
stream: _counterBloc.counterStreamState,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
snapshot.data!.counter.toString(),
style: Theme.of(context).textTheme.headlineMedium,
);
} else {
return const Text(
'0',
);
}
}),

That’s all for the vanilla implementation of the bloc . The complete code of this page is given below .


class CounterBlockVanillaImplementationDesignPage extends StatefulWidget {
const CounterBlockVanillaImplementationDesignPage({super.key, required this.title});

final String title;

@override
State<CounterBlockVanillaImplementationDesignPage> createState() => _CounterBlockVanillaImplementationDesignPageState();
}

class _CounterBlockVanillaImplementationDesignPageState extends State<CounterBlockVanillaImplementationDesignPage> {
// make the final constructor of of the bloc .

final CounterBloc _counterBloc = CounterBloc();

void _incrementCounter() {
_counterBloc.counterEventSink.add(CounterIncrementEvent());
}

void _decrementCounter() {
_counterBloc.counterEventSink.add(CounterDecrementEvent());
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: _counterBloc.counterStreamState,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
snapshot.data!.counter.toString(),
style: Theme.of(context).textTheme.headlineMedium,
);
} else {
return const Text(
'0',
);
}
}),
],
),
),
floatingActionButton: ButtonBar(
children: [
FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
const SizedBox(
width: 50,
),
FloatingActionButton(
onPressed: _decrementCounter,
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
],
));
}
}

Now for deep and detail’s understanding let’s talk about api’s using the bloc vanilla implementation .

For detailed information on api’s kindly read my article on api link is given below .

Now let’s start the api we will work on free api’s of jsonplaceholder

https://jsonplaceholder.typicode.com/albums

Now we have to make the code for fetching the api’s .

For interacting with the api’s kindly add the dependencies of the above package in your project .

i ) Data Model :

For working with the api we have to create the data model class . In this case i create a model class of the album model class . The model class contain’s the properties of the api’s and other function that are used to interact with the api’s like from json from map or json encode or json decode .

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

class AlbumModel {
int userId;
int id;
String title;
AlbumModel({
required this.userId,
required this.id,
required this.title,
});

AlbumModel copyWith({
int? userId,
int? id,
String? title,
}) {
return AlbumModel(
userId: userId ?? this.userId,
id: id ?? this.id,
title: title ?? this.title,
);
}

Map<String, dynamic> toMap() {
return <String, dynamic>{
'userId': userId,
'id': id,
'title': title,
};
}

factory AlbumModel.fromMap(Map<String, dynamic> map) {
return AlbumModel(
userId: map['userId'] as int,
id: map['id'] as int,
title: map['title'] as String,
);
}

String toJson() => json.encode(toMap());

factory AlbumModel.fromJson(String source) =>
AlbumModel.fromMap(json.decode(source) as Map<String, dynamic>);

@override
String toString() => 'AlbumModel(userId: $userId, id: $id, title: $title)';

@override
bool operator ==(covariant AlbumModel other) {
if (identical(this, other)) return true;

return other.userId == userId && other.id == id && other.title == title;
}

@override
int get hashCode => userId.hashCode ^ id.hashCode ^ title.hashCode;
}

Now we have to create a api service class in which we have to write the crud operation’s and url’s .

Import the http package and add the allias of the name http with it .

import 'package:http/http.dart' as http;

Extension Method :

Now it’s time to make the extension method on http response class .

extension ResponseCode on http.Response {
bool get isSuccess => statusCode == 200;
bool get isInsertSuccess => statusCode == 201;
bool get isNotFound => statusCode == 404;
}

Api Service :

Api service contain’s the url and the general queries of fetching the data from the api’s .



abstract class APIService {
String get baseURL => 'https://jsonplaceholder.typicode.com';
String get apiURL;
String get url => baseURL + apiURL;

dynamic fetch({String endPoint = '', Map<String, String>? headers}) async {
var response = await http.get(Uri.parse(url + endPoint), headers: headers);
if (response.isSuccess) {
return jsonDecode(response.body);
}
return null;
}

Future<bool> insert(Map<String, dynamic> map,
{Map<String, String>? headers}) async {
var response = await http.post(Uri.parse(url),
body: jsonEncode(map), headers: headers);
return response.isSuccess;
}

Future<bool> update(Map<String, dynamic> map,
{Map<String, String>? headers}) async {
var response =
await http.put(Uri.parse(url), body: jsonEncode(map), headers: headers);
return response.isSuccess;
}

Future<bool> delete(
{required String endPoint, Map<String, String>? headers}) async {
var response =
await http.delete(Uri.parse(url + endPoint), headers: headers);
return response.isSuccess;
}
}

Album Api Service :

Album api service is the specialized api service class and here we have to write the code for the crud operations of the api .


class AlbumService extends APIService {
static AlbumService? _albumService;
AlbumService._internal();
factory AlbumService() {
return _albumService ??= AlbumService._internal();
}

Future<List<AlbumModel>> fetchAlbums() async {
List albumList = await fetch();
return albumList.map((map) => AlbumModel.fromMap(map)).toList();
}

Future<AlbumModel> fetchAlbum(int albumId) async {
var map = await fetch(endPoint: '/$albumId');
return AlbumModel.fromMap(map);
}

Future<bool> insertAlbum(AlbumModel albumModel) async {
return await insert(albumModel.toMap());
}

@override
String get apiURL => '/albums';
}

Now that’s all for the code for interacting with the api’s .

Now work for the bloc with it’s vanilla implementation .

Album Api State :

Now we have to make all of the possible state’s of the album api’s .

Must add the immutable annotation with all of the possible state classes .

Make an abstract class of the album api state .

@immutable
abstract class AlbumApiState {
const AlbumApiState();
}

There are only four possible state’s for fetching api’s in our case like album initial state , album loading state , album loaded state , album error state .


@immutable
abstract class AlbumApiState {
const AlbumApiState();
}

@immutable
class AlbumApiInitialState extends AlbumApiState {}

@immutable
class AlbumApiLoadingState extends AlbumApiState {}

@immutable
class AlbumLoadedState extends AlbumApiState {
final List<AlbumModel> albums;
const AlbumLoadedState({required this.albums});
}

@immutable
class AlbumApiErrorState extends AlbumApiState {
final String message;
const AlbumApiErrorState({required this.message});
}

That’s all for the album api state class .

Album Api Events :

Now we have to make all of the possible event’s in our case of the api we only have the album api fetch event’s .

Make an abstract event of the album api event’s which become’s the parent of all of the possible event’s

Must add the immutable annotation with all of the possible event’s .

import 'package:flutter/material.dart';

@immutable
abstract class AlbumApiEvents {}

@immutable
class AlbumApiFetchEvent extends AlbumApiEvents {}

Here in our case we only fetch the data so i will make the album fetch event and it will extend the album event class .

That’s all for the album api event’s .

Album Api Bloc :

The album api bloc work’s as the middle layer that will emit the state on the sink of the event .

Make a class of the album api block

class AlbumApiBloc { }

Now we have to make the stream controller for both of the state’s and the event’s .

final StreamController<AlbumApiEvents> _albumEventStreamController =
StreamController<AlbumApiEvents>();
final StreamController<AlbumApiState> _albumStateStreamController =
StreamController<AlbumApiState>();

Make the getter of the stream of the album state and stream sink of the album event .


Stream<AlbumApiState> get albumStateStream =>
_albumStateStreamController.stream;

StreamSink<AlbumApiEvents> get albumEventSink =>
_albumEventStreamController.sink;

Now we have to make the constructor of the album api bloc class .

When the constructor of the album api bloc is called then we have to add the initial state in the state stream controller .

AlbumApiBloc() {
_albumStateStreamController.add(AlbumApiInitialState());
_albumEventStreamController.stream.listen(mapEventToState);
}

void mapEventToState(AlbumApiEvents albumApiEvents) async {
AlbumService albumService = AlbumService();

if (albumApiEvents is AlbumApiFetchEvent) {
_albumStateStreamController.add(AlbumApiInitialState());
try {
// Now it's time to add the loading widget
_albumStateStreamController.add(AlbumApiLoadingState());
// Now it's time to fetch the data using the api service
List<AlbumModel> albums = await albumService.fetchAlbums();
// now we have to add the above fetched album's in the album loaded state and ad the loaded state in the stream of states .
_albumStateStreamController.add(AlbumLoadedState(albums: albums));
} catch (e) {
_albumStateStreamController
.add(AlbumApiErrorState(message: e.toString()));
}
} else {}
}

The listen method will gives us event’s and there is only one event which is the fetch event then if the incoming event is fetch event then we will return the api states based on our requirement’s and if the incoming state is not fetch event then we will do nothing or null .

Now we will make the constructor of the album api service class .

Now we have to add the album initial state in the controller of the album state stream controller .

Now we try to fetch the api’s and add the album loading state in the controller of the album state stream controller .

After the data from the api’s is fetched successfully then we should add the album loaded state in the state stream controller or in case of error we will show the album api error state .

That’s all for the album api bloc .

Now we have to make the widget that would displayed on the screen .

Album Initial Widget :

The album initial widget will be displayed on the screen that will show the text of the “ click on floating Action button to fetch api’s ” .

This will displayed when the state is album initial state .

import 'package:flutter/material.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/bloc/album_api_bloc.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/events/album_events.dart';

class AlbumInitialWidget extends StatefulWidget {
const AlbumInitialWidget({super.key});

@override
State<AlbumInitialWidget> createState() => _AlbumInitialWidgetState();
}

class _AlbumInitialWidgetState extends State<AlbumInitialWidget> {
@override
Widget build(BuildContext context) {
return const Text(
"Click on floating Action button to fetch api's",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
);
}
}

Album Loading Widget :

The album Loading widget will display the circular progress indicator in the center of the screen .

import 'package:flutter/material.dart';

class AlbumLoadingWidget extends StatefulWidget {
const AlbumLoadingWidget({super.key});

@override
State<AlbumLoadingWidget> createState() => _AlbumLoadingWidgetState();
}

class _AlbumLoadingWidgetState extends State<AlbumLoadingWidget> {
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}

This widget is displayed when the state is album loading state .

Album Loaded Widget :

Album loaded widget that will displayed the data of the api’s on the screen this will displayed when the state is album loaded state .

import 'package:flutter/material.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/album_model.dart';

class ALbumLoadedWidget extends StatelessWidget {
const ALbumLoadedWidget({super.key, required this.albums});

final List<AlbumModel> albums;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: albums.length,
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(
child: Text(albums[index].id.toString()),
),
title: Text(albums[index].title),
trailing: Text(albums[index].userId.toString()),
),
);
}
}

Album Error Widget :

Album error widget that will show the exception or the error on the screen if there was any exception like network error or many other’s .

import 'package:flutter/material.dart';

class AlbumErrorWidget extends StatelessWidget {
const AlbumErrorWidget({super.key, required this.errorMessage});
final String errorMessage;

@override
Widget build(BuildContext context) {
return Center(
child: Text(
errorMessage,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
);
}
}

Now we have to write the code for the page where we can display these widget’s for that purpose i have created a state full widget named as the Album Vanilla Api Page Design .

import 'package:flutter/material.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/bloc/album_api_bloc.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/events/album_events.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/states/album_state.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/widgets/album_error_widget.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/widgets/album_initial_widget.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/widgets/album_loaded_widget.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/widgets/album_loading_widgets.dart';

class AlbumVanillaApiPageDesign extends StatefulWidget {
const AlbumVanillaApiPageDesign({super.key});

@override
State<AlbumVanillaApiPageDesign> createState() =>
_AlbumVanillaApiPageDesignState();
}

class _AlbumVanillaApiPageDesignState extends State<AlbumVanillaApiPageDesign> {

void fetchApiData() {

}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Album Api's Vanilla implementation"),
centerTitle: true,
),
body: Center(

),
floatingActionButton: FloatingActionButton(
onPressed: fetchApiData,
child: const Icon(Icons.download_for_offline_outlined),
),
);
}
}

Now we have to write the code for the fetch api data .

Make the constructor of the album api bloc .

  final AlbumApiBloc _albumApiBloc = AlbumApiBloc();

Now we have to write the code for the fetch album api .

In this function we have to write the only one following line .

void fetchApiData() {
_albumApiBloc.albumEventSink.add(AlbumApiFetchEvent());
}

This above line will add the album fetch event in the stream of the album event sink that we have created above in the album api bloc class .

Now we have to add the stream builder and return the appropriate widget regarding with the state’s .

StreamBuilder(
stream: _albumApiBloc.albumStateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
switch (snapshot.data.runtimeType) {
case AlbumApiInitialState:
return const AlbumInitialWidget();
case AlbumApiLoadingState:
return const AlbumLoadingWidget();
case AlbumLoadedState:
return ALbumLoadedWidget(
albums: (snapshot.data as AlbumLoadedState).albums);
default:
return AlbumErrorWidget(
errorMessage:
(snapshot.data as AlbumApiErrorState).message);
}
} else {
return const Text("Oops !! NO Data Found ");
}
},
),

Now we have to check the incoming state from the state stream using the runtime type method to check the type of the given state .

If the state is album initial state then we will return the album initial widget .

If the state is loading state then we will return the album loading widget .

If the data is successfully fetched from the api then we will return the loaded widget and pass the albums in it .

If there is some issues while fetching the data from the api’s then the error page will be returned and the exception will be displayed on the screen .

The complete code of the Album vanilla page design is given below .

import 'package:flutter/material.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/bloc/album_api_bloc.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/events/album_events.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/states/album_state.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/widgets/album_error_widget.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/widgets/album_initial_widget.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/widgets/album_loaded_widget.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/widgets/album_loading_widgets.dart';

class AlbumVanillaApiPageDesign extends StatefulWidget {
const AlbumVanillaApiPageDesign({super.key});

@override
State<AlbumVanillaApiPageDesign> createState() =>
_AlbumVanillaApiPageDesignState();
}

class _AlbumVanillaApiPageDesignState extends State<AlbumVanillaApiPageDesign> {
final AlbumApiBloc _albumApiBloc = AlbumApiBloc();

void fetchApiData() {
print("entered");
_albumApiBloc.albumEventSink.add(AlbumApiFetchEvent());
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Album Api's Vanilla implementation"),
centerTitle: true,
),
body: Center(
child: StreamBuilder(
stream: _albumApiBloc.albumStateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
switch (snapshot.data.runtimeType) {
case AlbumApiInitialState:
return const AlbumInitialWidget();
case AlbumApiLoadingState:
return const AlbumLoadingWidget();
case AlbumLoadedState:
return ALbumLoadedWidget(
albums: (snapshot.data as AlbumLoadedState).albums);
default:
return AlbumErrorWidget(
errorMessage:
(snapshot.data as AlbumApiErrorState).message);
}
} else {
return const Text("Oops !! NO Data Found ");
}
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: fetchApiData,
child: const Icon(Icons.download_for_offline_outlined),
),
);
}
}

That’s all for the vanilla implementation .

Now let’s add the dependencies of the flutter bloc package and implement the flutter bloc in our project .

If you are using the vscode for the flutter development then add the given below extension that will help’s to make the bloc automatically .

This above extension will automatically make all of three classes of the state , event and bloc respectively or you may create these by youself .

This will made the above three class . I will explain it ( The things that was discussed above in vanilla implementation will not be discussed again ) .

Counter State :

This class contain’s all of the possible state’s of the ( Same as we discussed in vanilla bloc implementation ) .

part of 'counter_bloc.dart';

@immutable
class CounterState {
final int count;
const CounterState({required this.count});
}


// You either make the two state's like the intial counter state and the counter updated state .

Counter Events :

This class contain’s all of the possible event’s of the counter . In our case we have only two event’s . (Increment and decrement) .

part of 'counter_bloc.dart';

@immutable
sealed class CounterEvent {}

@immutable
class CounterIncrementEvent extends CounterEvent {}

@immutable
class CounterDecrementEvent extends CounterEvent {}

Counter Bloc :

The counter bloc act as a middle layer between the model and the event in which it will emit the state on the click event’s .

import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';

part 'counter_event.dart';
part 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
// final CounterState _counterState = const CounterState(count: 0);
CounterBloc() : super(const CounterState(count: 0)) {
on<CounterEvent>((event, emit) {
if (event is CounterIncrementEvent) {
emit(CounterState(count: state.count + 1));
} else if (event is CounterDecrementEvent) {
emit(CounterState(count: state.count - 1));
}
});
}
}

Here block is the special type of the stream that will input’s the event’s and the state’s . Now you have to pass the initial value or the state in the constructor of the super .

Now you have to emit the updated state based on the event’s .

Check this event if this is the counter increment event then you have to emit the updated state in which you have to add one in the value of the counter variable .

If the event is counter decrement event then you have to emit the updated the state in which you have to minus one in the value of the counter variable .

That’s all for the counter bloc .

Now you have to create the counter page design for the user interface of the counter app .

For that purpose i make a state full widget of the counter page design .

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/counter_bloc.dart';

class CounterPageDesign extends StatefulWidget {
const CounterPageDesign({super.key});

@override
State<CounterPageDesign> createState() => _CounterPageDesignState();
}

class _CounterPageDesignState extends State<CounterPageDesign> {

@override
Widget build(BuildContext context) {
void increment() {

}

void decrement() {

}

return Scaffold(
appBar: AppBar(
title: Text("Counter Page Design"),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Code for displaying the counter value will be placed here")
SizedBox(
width: 700,
height: 300,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkWell(
onTap: decrement,
child: Container(
width: 200,
height: 60,
alignment: Alignment.center,
color: Colors.blue,
child: const Text("Decrement"),
),
),
InkWell(
onTap: increment,
child: Container(
width: 200,
height: 60,
alignment: Alignment.center,
color: Colors.amber,
child: const Text("Increment"),
),
)
],
),
)
],
),
),
);
}
}

Now we have to add the code for the text of the counter variable to display it and also write the code for the increment and decrement method .

read :

Obtain a value from the nearest ancestor provider of type [T] .

This method is the opposite of [watch].
It will not make widget rebuild when the value changes and cannot be called inside [StatelessWidget.build]/[State.build] build method .

Now you have to add the template type of the counter bloc in it and add the counter increment and decrement in it repectively .

 void increment() {
context.read<CounterBloc>().add(CounterIncrementEvent());
}

void decrement() {
context.read<CounterBloc>().add(CounterDecrementEvent());
}

Block Builder :

BlocBuilder handles building a widget in response to new states.

Use the bloc builder where you want’s to display the change .

 BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(state.count.toString());
},
),

Call the above function’s on the click event’s of the button’s .

The complete code of the counter page is given below .

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/counter_bloc.dart';

class CounterPageDesign extends StatefulWidget {
const CounterPageDesign({super.key});

@override
State<CounterPageDesign> createState() => _CounterPageDesignState();
}

class _CounterPageDesignState extends State<CounterPageDesign> {

@override
Widget build(BuildContext context) {
void increment() {
context.read<CounterBloc>().add(CounterIncrementEvent());
}

void decrement() {
context.read<CounterBloc>().add(CounterDecrementEvent());
}

return Scaffold(
appBar: AppBar(
title: const Text("Counter Page Design"),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(state.count.toString());
},
),
SizedBox(
width: 700,
height: 300,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkWell(
onTap: decrement,
child: Container(
width: 200,
height: 60,
alignment: Alignment.center,
color: Colors.blue,
child: const Text("Decrement"),
),
),
InkWell(
onTap: increment,
child: Container(
width: 200,
height: 60,
alignment: Alignment.center,
color: Colors.amber,
child: const Text("Increment"),
),
)
],
),
)
],
),
),
);
}
}

Now we have to add the following things in the main page .

At the top of the widget tree you have to add the bloc provider in which you have to add the counter bloc as it’s template type so for this purpose i will add this in the home property of the material app on the main page .

 home: BlocProvider<CounterBloc>(
create: (context) => CounterBloc(),
child: const CounterPageDesign(),
),

In the create method of the block provider you have to add the bloc that you have created . As the child you have to pass the child on which you use the above bloc .

The complete code for the main page is given below .


void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: BlocProvider<CounterBloc>(
create: (context) => CounterBloc(),
child: const CounterPageDesign(),
),

);
}
}

You may also use the Multi bloc provider if you are using different types of the bloc inside your app .

 home: MultiBlocProvider(
providers: [
BlocProvider<CounterBloc>(
create: (context) => CounterBloc(),
child: CounterPageDesign(),
),
BlocProvider<CounterBloc>(
create: (context) => CounterBloc(),
child: const CounterPageDesign(),
),
],
child: const CounterPageDesign(),
),

Merges multiple BlocProvider widgets into one widget tree.

MultiBlocProvider improves the readability and eliminates the need to nest multiple BlocProviders.

That’s all for the counter app using the flutter bloc package .

Now it’s time to work on the api’s using the flutter bloc package .

Now for deep and detail’s understanding let’s talk about api’s using the flutter bloc package .

For detailed information on api’s kindly read my article on api link is given below .

Now let’s start the api we will work on free api’s of jsonplaceholder

https://jsonplaceholder.typicode.com/albums

Now we have to make the code for fetching the api’s .

For interacting with the api’s kindly add the dependencies of the above package in your project .

i ) Data Model :

For working with the api we have to create the data model class . In this case i create a model class of the album model class . The model class contain’s the properties of the api’s and other function that are used to interact with the api’s like from json from map or json encode or json decode .

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
class AlbumModel {
int userId;
int id;
String title;
AlbumModel({
required this.userId,
required this.id,
required this.title,
});
AlbumModel copyWith({
int? userId,
int? id,
String? title,
}) {
return AlbumModel(
userId: userId ?? this.userId,
id: id ?? this.id,
title: title ?? this.title,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'userId': userId,
'id': id,
'title': title,
};
}
factory AlbumModel.fromMap(Map<String, dynamic> map) {
return AlbumModel(
userId: map['userId'] as int,
id: map['id'] as int,
title: map['title'] as String,
);
}
String toJson() => json.encode(toMap());
factory AlbumModel.fromJson(String source) =>
AlbumModel.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'AlbumModel(userId: $userId, id: $id, title: $title)';
@override
bool operator ==(covariant AlbumModel other) {
if (identical(this, other)) return true;
return other.userId == userId && other.id == id && other.title == title;
}
@override
int get hashCode => userId.hashCode ^ id.hashCode ^ title.hashCode;
}

Now we have to create a api service class in which we have to write the crud operation’s and url’s .

Import the http package and add the allias of the name http with it .

import 'package:http/http.dart' as http;

Extension Method :

Now it’s time to make the extension method on http response class .

extension ResponseCode on http.Response {
bool get isSuccess => statusCode == 200;
bool get isInsertSuccess => statusCode == 201;
bool get isNotFound => statusCode == 404;
}

Api Service :

Api service contain’s the url and the general queries of fetching the data from the api’s .

abstract class APIService {
String get baseURL => 'https://jsonplaceholder.typicode.com';
String get apiURL;
String get url => baseURL + apiURL;

dynamic fetch({String endPoint = '', Map<String, String>? headers}) async {
var response = await http.get(Uri.parse(url + endPoint), headers: headers);
if (response.isSuccess) {
return jsonDecode(response.body);
}
return null;
}
Future<bool> insert(Map<String, dynamic> map,
{Map<String, String>? headers}) async {
var response = await http.post(Uri.parse(url),
body: jsonEncode(map), headers: headers);
return response.isSuccess;
}
Future<bool> update(Map<String, dynamic> map,
{Map<String, String>? headers}) async {
var response =
await http.put(Uri.parse(url), body: jsonEncode(map), headers: headers);
return response.isSuccess;
}
Future<bool> delete(
{required String endPoint, Map<String, String>? headers}) async {
var response =
await http.delete(Uri.parse(url + endPoint), headers: headers);
return response.isSuccess;
}
}

Album Api Service :

Album api service is the specialized api service class and here we have to write the code for the crud operations of the api .

class AlbumService extends APIService {
static AlbumService? _albumService;
AlbumService._internal();
factory AlbumService() {
return _albumService ??= AlbumService._internal();
}
Future<List<AlbumModel>> fetchAlbums() async {
List albumList = await fetch();
return albumList.map((map) => AlbumModel.fromMap(map)).toList();
}
Future<AlbumModel> fetchAlbum(int albumId) async {
var map = await fetch(endPoint: '/$albumId');
return AlbumModel.fromMap(map);
}
Future<bool> insertAlbum(AlbumModel albumModel) async {
return await insert(albumModel.toMap());
}
@override
String get apiURL => '/albums';
}

Now that’s all for the code for interacting with the api’s .

Now we have to write the code flutter bloc package .

Using the extension of the bloc on vs code you can create the album api bloc classes or you may write yourself .

Album State :

In this class we have to make all of the possible states . In our case of api we only have the following 4 states .

part of 'album_api_bloc.dart';

// Make all of the possible states .
// In our case of api we only have the following 4 states .


@immutable
sealed class AlbumApiState {
const AlbumApiState();
}

final class AlbumApiInitialState extends AlbumApiState {}

final class AlbumApiLoadingState extends AlbumApiState {}

final class AlbumApiLoadedState extends AlbumApiState {
final List<AlbumModel> albums;

const AlbumApiLoadedState({required this.albums});
}

final class AlbumApiErrorState extends AlbumApiState {
final String errorMessage;
const AlbumApiErrorState({required this.errorMessage});
}

The detailed about these four state’s are discussed above in detail’s in the vanilla implementation .

Album Event :

In this clas we have to make all of the possible event’s .
In our case we have only one event of fetch Albums ( or you make two event’s for albums fetch and album fetch for single album to be fetched based on the given endpoint ) .

Must use the immutable annotation with all of the event’s .

part of 'album_api_bloc.dart';

@immutable
sealed class AlbumApiEvent {}

@immutable
final class FetchAlbumsEvent extends AlbumApiEvent {}

That’s all for the album event class . The detailed discussion on this class is already done in the bloc vanilla implementation .

Album Api Bloc :

The album api bloc act as the middle layer that will emit the state on every event (in our case we have only one fetch event ) .

This class will extend the block class in which it input’s the two template type’s of the album state and the album event’s .

abstract class Bloc<Event, State> extends BlocBase<State> implements BlocEventSink<Event>

Takes a Stream of Events as input and transforms them into a Stream of States as output.

The bloc is the special type of the stream which input’s the the event’s and their corresponding states . Perform all your work’d here on the bases of which the state will be returned .

Now we have to make the final constructor of the (specialized) service class .

final AlbumService _albumService = AlbumService();

In the super constructor of the super you have to pass the initial state .

class AlbumApiBloc extends Bloc<AlbumApiEvent, AlbumApiState> {

AlbumApiBloc() : super(AlbumApiInitialState()) {
on<FetchAlbumsEvent>((event, emit) async {

});

}
}

Now we have to emit the loading state ( this is called when the user click’s on the button and sink the fetch albums event in the stream of the states ) .

emit(AlbumApiLoadingState());

Now we have to call the fetch albums method of the album service class and assign these fetched albums to the variable .

List<AlbumModel> albums = await _albumService.fetchAlbums();

Now we successfully fetched the album . Now it’s time to emit the album api loaded state .

emit(AlbumApiLoadedState(albums: albums));

We must use the try catch block for handling the exception’s .

In case of the error we will show the api error page display the catched exception in it .

try {
List<AlbumModel> albums = await _albumService.fetchAlbums();
emit(AlbumApiLoadedState(albums: albums));
} catch (e) {
emit(AlbumApiErrorState(errorMessage: e.toString()));
}

That’s all for the album api bloc the complete code of this class is given below .

import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';

import '../album_model.dart';
import '../album_service.dart';

part 'album_api_event.dart';
part 'album_api_state.dart';

class AlbumApiBloc extends Bloc<AlbumApiEvent, AlbumApiState> {
final AlbumService _albumService = AlbumService();
AlbumApiBloc() : super(AlbumApiInitialState()) {
on<FetchAlbumsEvent>((event, emit) async {

emit(AlbumApiLoadingState());
try {
List<AlbumModel> albums = await _albumService.fetchAlbums();
emit(AlbumApiLoadedState(albums: albums));
} catch (e) {
emit(AlbumApiErrorState(errorMessage: e.toString()));
}
});

}
}

Now we have to make the widget’s that will be displayed on the screen regarding to the state .

Album Initial Widget :

The album initial widget will be displayed on the screen that will show the text of the “ click on floating Action button to fetch api’s ” .

This will displayed when the state is album initial state .

import 'package:flutter/material.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/bloc/album_api_bloc.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/events/album_events.dart';


class AlbumInitialWidget extends StatelessWidget {
const AlbumInitialWidget({super.key});

@override
Widget build(BuildContext context) {
return Center(
child: InkWell(
onTap: () {
context.read<AlbumApiBloc>().add(FetchAlbumsEvent());
},
child: Container(
width: 300,
height: 70,
color: Colors.blueGrey,
alignment: Alignment.center,
child: const Text(
"Fetch Data",
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
),
));
}
}

Album Loading Widget :

The album Loading widget will display the circular progress indicator in the center of the screen .

import 'package:flutter/material.dart';

class AlbumLoadingWidget extends StatelessWidget {
const AlbumLoadingWidget({super.key});

@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}

This widget is displayed when the state is album loading state .

Album Loaded Widget :

Album loaded widget that will displayed the data of the api’s on the screen this will displayed when the state is album loaded state .

import 'package:flutter/material.dart';
import 'package:flutter_bloc_practice_f12/api_vanilla_implementation/album_model.dart';

class AlbumLoadedWidget extends StatelessWidget {
const AlbumLoadedWidget({super.key, required this.albums});
final List<AlbumModel> albums;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(
child: Text(albums[index].id.toString()),
),
title: Text(albums[index].title),
trailing: Text(albums[index].userId.toString()),
));
}
}

Album Error Widget :

Album error widget that will show the exception or the error on the screen if there was any exception like network error or many other’s .

import 'package:flutter/material.dart';

class AlbumErrorWidget extends StatelessWidget {
const AlbumErrorWidget({super.key, required this.errorMessage});
final String errorMessage;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Text(
errorMessage,
style: const TextStyle(color: Colors.white, fontSize: 46),
),
);
}
}

That’s all for the widget that will be display on the screen .

Now we have to write the code for the main page where the data of the api will be displayed and these above widget are used .

For that purpose i make a sate full widget of the name album api design .

In which i use the bloc builder and gives the album api bloc ( that we have created above ) and the Album Api State as the template types .

Now in the builder method it will give us the album state and we have to return the widget corresponding to the states .

Now we have to check the states .

If the state is album initial state then we will return the album initial widget .

If the state is album loading state then we will return the album loading widget .

If the state is album loaded state then we will return the album loaded widget in the we will give the album’s that are declared before in the album loaded state class .

Now if there is some error as the default case i will return the album error page and display the error on it using the error message variable that we have declared in the album error state class .


class AlbumApiDesign extends StatefulWidget {
const AlbumApiDesign({super.key});

@override
State<AlbumApiDesign> createState() => _AlbumApiDesignState();
}

class _AlbumApiDesignState extends State<AlbumApiDesign> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Album API Design Page"),
centerTitle: true,
),
body: Center(
// Bloc builder is used to build the widget's base on the bloc and return the corresponding widget's based on the state .
child: BlocBuilder<AlbumApiBloc, AlbumApiState>(
builder: (context, albumState) {
if (albumState is AlbumApiInitialState) {
return const AlbumInitialWidget();
} else if (albumState is AlbumApiLoadingState) {
return const AlbumLoadingWidget();
} else if (albumState is AlbumApiLoadedState) {
return AlbumLoadedWidget(albums: albumState.albums);
} else {
return AlbumErrorWidget(
errorMessage:
(albumState as AlbumApiErrorState).errorMessage);
}
},
),
),
);
}
}

Now the only remaining thing is to add the block provider at the top of the widget tree . So i use it in the home property of the main page .

 home: BlocProvider<AlbumApiBloc>(
create: (context) => AlbumApiBloc(),

child: const AlbumApiDesign(),
),

In which as the template type of the bloc provider i will provide the album api bloc and return the same from the create method that is responsible for creating the given bloc .

As the child parameter of the bloc provider i use the album api design page on which i use the above provided bloc .

That’s all for the api using the flutter bloc package .

That’s all for the bloc .

The complete of the bloc is provided at my github link is given below .

I hope you have learned a lot of new things 😊

Thanks for reading 📚

--

--