BLoC Pattern in Flutter explained with Real Example

Jamie
ITNEXT
Published in
5 min readOct 5, 2020

--

In this article we will learn BLoC pattern in flutter for State Management with a simple real world example. You can find the second part of this tutorial here.

BLOC Flutter State Management

Watch Video Tutorials

Subscribe to my channel for more videos.

For this example, we will try to consume a web service. The service we are going to use is

https://jsonplaceholder.typicode.com/albums

What BLoC does?

BLoC helps to separate you presentation and business logic.

So in simple terms, we will write all our business logic inside the bloc file.

So what basically Bloc does is, it will take an event and a state and return a new state which we will use to update the UI.

You don’t need to maintain any local state in your application if you have BLoC.

Let’s start…

We will first write a service class that will consume the web service.

Add Dependencies

Open your pubspec.yaml file add the dependencies

Add all the dependencies needed for this example

dependencies:
flutter:
sdk: flutter cupertino_icons: ^0.1.2
flutter_bloc: ^6.0.4
equatable: ^1.2.4
http: ^0.12.2

http package to get data from the web service.

flutter_bloc for using the BLoC pattern.

equatable for comparing objects.

Let’s create the model classes for the service response first.

Model Class

Below is the response class for the service response

import 'dart:convert';List<Album> albumFromJson(String str) =>
List<Album>.from(json.decode(str).map((x) => Album.fromJson(x)));
String albumToJson(List<Album> data) =>
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
class Album {
Album({
this.userId,
this.id,
this.title,
});int userId;
int id;
String title;
factory Album.fromJson(Map<String, dynamic> json) => Album(
userId: json["userId"],
id: json["id"],
title: json["title"],
);Map<String, dynamic> toJson() => {
"userId": userId,
"id": id,
"title": title,
};
}

Once we have the model classes, we are ready for the service consumption.

Let’s create the services file and get the data.

Create a new file named “services.dart” and copy the below.

import 'package:flutter_demos/model/albums_list.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';abstract class AlbumsRepo {
Future<List<Album>> getAlbumList();
}class AlbumServices implements AlbumsRepo {
//
static const _baseUrl = 'jsonplaceholder.typicode.com';
static const String _GET_ALBUMS = '/albums';@override
Future<List<Album>> getAlbumList() async {
Uri uri = Uri.https(_baseUrl, _GET_ALBUMS);
Response response = await http.get(uri);
List<Album> albums = albumFromJson(response.body);
return albums;
}
}

Now let’s use BLOC to get the data to the UI.

Events

Here we have only one event — fetchAlbums.

Let’s create a class named events.dart and add the below enum.

enum AlbumEvents {
fetchAlbums,
}

States

Below are the states when we start getting the albums from the service.

So normally we will have states…

  1. AlbumsInitState
  2. AlbumsLoading
  3. AlbumsLoaded
  4. AlbumsListError

create a file named ‘states.dart’ and copy the below code.

import 'package:equatable/equatable.dart';
import 'package:flutter_demos/model/albums_list.dart';
abstract class AlbumsState extends Equatable {
@override
List<Object> get props => [];
}
class AlbumsInitState extends AlbumsState {}class AlbumsLoading extends AlbumsState {}class AlbumsLoaded extends AlbumsState {
final List<Album> albums;
AlbumsLoaded({this.albums});
}
class AlbumsListError extends AlbumsState {
final error;
AlbumsListError({this.error});
}

Here we are extending the ‘Equatable’ class which helps to compare objects. It needs an override and we can supply properties to which the comparison needs to do to find if two objects are equal. For simplicity we will keep it empty.

Bloc for Getting Albums

Let’s create a class named “AlbumsBloc” which extends BLoC.

import 'dart:io';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_demos/api/exceptions.dart';
import 'package:flutter_demos/api/services.dart';
import 'package:flutter_demos/bloc/albums/events.dart';
import 'package:flutter_demos/bloc/albums/states.dart';
import 'package:flutter_demos/model/albums_list.dart';class AlbumsBloc extends Bloc<AlbumEvents, AlbumsState> {

final AlbumsRepo albumsRepo;
List<Album> albums;AlbumsBloc({this.albumsRepo}) : super(AlbumsInitState());@override
Stream<AlbumsState> mapEventToState(AlbumEvents event) async* {
switch (event) {
case AlbumEvents.fetchAlbums:
yield AlbumsLoading();
try {
albums = await albumsRepo.getAlbumList();
yield AlbumsLoaded(albums: albums);
} on SocketException {
yield AlbumsListError(
error: NoInternetException('No Internet'),
);
} on HttpException {
yield AlbumsListError(
error: NoServiceFoundException('No Service Found'),
);
} on FormatException {
yield AlbumsListError(
error: InvalidFormatException('Invalid Response format'),
);
} catch (e) {
yield AlbumsListError(
error: UnknownException('Unknown Error'),
);
}break;
}
}
}

In the above bloc class, you can see that we have an instance variable of ‘AlbumsRepo’.

You need to supply an initial state to the BLoC constructor.

So we have the ‘AlbumsInitState’.

The ‘mapEventToState’ is an async * function, that means it can return data without terminating the function. Here we use yield to return one of the album states at appropriate moment.

i.e when the loading starts…the above function will return AlbumsLoading state to the UI, then when the data is returned ‘AlbumsLoaded’ is returned along with the albums, then if there is any error, ‘AlbumsListError’ is returned with appropriate exceptions and finally here is our exception class.

class NoInternetException {
var message;
NoInternetException(this.message);
}class NoServiceFoundException {
var message;
NoServiceFoundException(this.message);
}class InvalidFormatException {
var message;
InvalidFormatException(this.message);
}class UnknownException {
var message;
UnknownException(this.message);
}

Dart allows to throw any class as exceptions if you want.

Screen

Now let’s create a screen that will listen to these events.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_demos/bloc/albums/bloc.dart';
import 'package:flutter_demos/bloc/albums/events.dart';
import 'package:flutter_demos/bloc/albums/states.dart';
import 'package:flutter_demos/model/albums_list.dart';
import 'package:flutter_demos/widgets/error.dart';
import 'package:flutter_demos/widgets/list_row.dart';
import 'package:flutter_demos/widgets/loading.dart';
class AlbumsScreen extends StatefulWidget {
@override
_AlbumsScreenState createState() => _AlbumsScreenState();
}
class _AlbumsScreenState extends State<AlbumsScreen> {
//
@override
void initState() {
super.initState();
_loadAlbums();
}
_loadAlbums() async {
context.bloc<AlbumsBloc>().add(AlbumEvents.fetchAlbums);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Albums'),
),
body: Container(
child: _body(),
),
);
}
_body() {
return Column(
children: [
BlocBuilder<AlbumsBloc, AlbumsState>(
builder: (BuildContext context, AlbumsState state) {
if (state is AlbumsListError) {
final error = state.error;
String message = '${error.message}\nTap to Retry.';
return ErrorTxt(
message: message,
onTap: _loadAlbums,
);
}
if (state is AlbumsLoaded) {
List<Album> albums = state.albums;
return _list(albums);
}
return Loading();
}),
],
);
}
Widget _list(List<Album> albums) {
return Expanded(
child: ListView.builder(
itemCount: albums.length,
itemBuilder: (_, index) {
Album album = albums[index];
return ListRow(album: album);
},
),
);
}
}

The AlbumsScreen we created has a widget called BlocBuilder which has types ‘AlbumsBloc and AlbumsState’. This widget will listen to the changes from the bloc file and update the UI.

Here you can see that the BlocBuilder has a ‘builder’ property which has a state variable that gets us access to the returned state.

So how do we trigger it.

context.bloc<AlbumsBloc>().add(AlbumEvents.fetchAlbums);

The above call will trigger the ‘fetchAlbums’ event which will trigger the BLoC and returns the state. Initially it will return AlbumLoading State, then if the call is success, it will return AlbumsLoaded state etc. Rest is simple, Update the UI based on the state. Here we are showing a list of albums when the call succeeds.

The Main Part

The BlocBuilder receives the events to update the UI. But there should be a provider, correct. So you need to wrap the AlbumsScreen with the BlocProvider. So the idea is which ever screen that wants to listen to the AlbumBloc updates should wrap itself with the BlocProvider like below.

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Bloc Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: BlocProvider(
create: (context) => AlbumsBloc(albumsRepo: AlbumServices()),
child: AlbumsScreen(),
),
);
}
}

Here you can see ‘AlbumsBloc’ is receiving the ‘AlbumServices’ repo which fetches the albums.

That’s it.

Please clap if you like this post…

Remember, you can hit up to 50 claps.

Buy me a Coffee if you find my article useful

Source code

https://bitbucket.org/vipinvijayan1987/tutorialprojects/src/BlocSimple/FlutterTutorialProjects/flutter_demos/

--

--