BLOC Pattern in Flutter explained with Real Example

By | October 5, 2020

In this article we will learn BLoc pattern in flutter for State Management with a simple real world example.

BLOC for State Management in  Flutter

BLOC for State Management in Flutter

Watch Video Tutorial

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…

  • AlbumsInitState
  • AlbumsLoading
  • AlbumsLoaded
  • 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 a 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.

Flutter BLOC

Flutter BLOC


Source code

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


One thought on “BLOC Pattern in Flutter explained with Real Example

  1. Лев

    Streams represent flux of data and events, and what it’s important for? With Streams, you can listen to data and event changes, and just as well, deal with what’s coming from the Stream with listeners. How it can be applied to Flutter? For example, we have a Widget in Flutter called StreamBuilder that builds itself based on the latest snapshot of interaction with a Stream, and when there’s a new flux of data the Widget reload to deal with the new data. Widget Weekly of Flutter Dev Channel offers great content about how the StreamBuilder works. And about Sinks? If we have an output of a data flux, we also need an input, that’s what Sinks is used for, seems simple right? Now let’s see about the BLoC pattern and how can we combine both concepts into a great Flutter app.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *