To understand flutter_bloc we will create a demo of hitting an API that brings in the football players details. We will create multiple states for an event and see how bloc provider and bloc builder are used to manage state of the app.

The finished product will look like this:

How to implement BLoC pattern using flutter_bloc package

Ignore the UI 😅. This app is all about understanding flutter_bloc.

BLoC Architecture

A BLoC takes a stream of events as input and transforms them into a stream of states as output_._

How to implement BLoC pattern using flutter_bloc package

Events???States???

Events are actions that occurs as a result of the user interaction with the app such as button pressed. Events are dispatched and converted to States with the help of a function named mapEventToState .State is the information that can be read synchronously when the widget is built and might change during the lifetime of the widget.

Quick overview of the package

In flutter_bloc package we have :

BlocProvider

BlocProvider is a Flutter widget(Inherited widget) which provides a bloc to its child via BlocProvider.of(context). Child widget gets updated whenever there is any change in the bloc(Business Logic Components).

BlocProvider(
  builder: (BuildContext context) => BlocA(),
  child: ChildA(),
);

BlocBuilder

BlocBuilder is a Flutter widget which requires a Bloc and a builderfunction. BlocBuilder handles building a widget in response to new states.

Let’s Start

Setup

The first step is to add the required plugins as a dependency in the pubspec.yaml file.

dependencies:
  http: ^0.12.0+2
  flutter_bloc: ^0.21.0

Next, run flutter packages get to install all the dependencies.

API

For this application we will be hitting the EA Sports API.

Our base Url is https://www.easports.com/fifa/ultimate-team/api/fut/item?"

We’ll be focusing on two endpoints:

  • /api/fut/item?country=$countryId to get list of players for a given country.
  • /api/fut/item?name=$name to get list of players for a given name belonging to any country.

Data Model

Open https://www.easports.com/fifa/ultimate-team/api/fut/item?country=52 in your browser and you’ll see the following response:

How to implement BLoC pattern using flutter_bloc package

Copy this response and use https://javiercbk.github.io/json_to_dart/ to get the dart code. Create a models directory inside lib directory and create a file inside that api_models.dart and paste the copied code there.

class ApiResult {
  int page;
  int totalPages;
  int totalResults;
  String type;
  int count;
  List<Players> items;

  ApiResult(
      {this.page,
        this.totalPages,
        this.totalResults,
        this.type,
        this.count,
        this.items});

  ApiResult.fromJson(Map<String, dynamic> json) {
    page = json['page'];
    totalPages = json['totalPages'];
    totalResults = json['totalResults'];
    type = json['type'];
    count = json['count'];
    if (json['items'] != null) {
      items = new List<Players>();
      json['items'].forEach((v) {
        items.add(new Players.fromJson(v));
      });
    }
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['page'] = this.page;
    data['totalPages'] = this.totalPages;
    data['totalResults'] = this.totalResults;
    data['type'] = this.type;
    data['count'] = this.count;
    if (this.items != null) {
      data['items'] = this.items.map((v) => v.toJson()).toList();
    }
    return data;
  }
}

class Players {
  String commonName;
  String firstName;
  String lastName;
  League league;
  Nation nation;
  Club club;
  Headshot headshot;
  String position;
  int composure;
  String playStyle;
  Null playStyleId;
  int height;
  int weight;
  String birthdate;
  int age;
  int acceleration;
  int aggression;
  int agility;
  int balance;
  int ballcontrol;
  String foot;
  int skillMoves;
  int crossing;
  int curve;
  int dribbling;
  int finishing;
  int freekickaccuracy;
  int gkdiving;
  int gkhandling;
  int gkkicking;
  int gkpositioning;
  int gkreflexes;
  int headingaccuracy;
  int interceptions;
  int jumping;
  int longpassing;
  int longshots;
  int marking;
  int penalties;
  int positioning;
  int potential;
  int reactions;
  int shortpassing;
  int shotpower;
  int slidingtackle;
  int sprintspeed;
  int standingtackle;
  int stamina;
  int strength;
  int vision;
  int volleys;
  int weakFoot;
  List<String> traits;
  List<String> specialities;
  String atkWorkRate;
  String defWorkRate;
  Null playerType;
  List<Attributes> attributes;
  String name;
  int rarityId;
  bool isIcon;
  String quality;
  bool isGK;
  String positionFull;
  bool isSpecialType;
  Null contracts;
  Null fitness;
  Null rawAttributeChemistryBonus;
  Null isLoan;
  Null squadPosition;
  IconAttributes iconAttributes;
  String itemType;
  Null discardValue;
  String id;
  String modelName;
  int baseId;
  int rating;

  Players(
      {this.commonName,
        this.firstName,
        this.lastName,
        this.league,
        this.nation,
        this.club,
        this.headshot,
        this.position,
        this.composure,
        this.playStyle,
        this.playStyleId,
        this.height,
        this.weight,
        this.birthdate,
        this.age,
        this.acceleration,
        this.aggression,
        this.agility,
        this.balance,
        this.ballcontrol,
        this.foot,
        this.skillMoves,
        this.crossing,
        this.curve,
        this.dribbling,
        this.finishing,
        this.freekickaccuracy,
        this.gkdiving,
        this.gkhandling,
        this.gkkicking,
        this.gkpositioning,
        this.gkreflexes,
        this.headingaccuracy,
        this.interceptions,
        this.jumping,
        this.longpassing,
        this.longshots,
        this.marking,
        this.penalties,
        this.positioning,
        this.potential,
        this.reactions,
        this.shortpassing,
        this.shotpower,
        this.slidingtackle,
        this.sprintspeed,
        this.standingtackle,
        this.stamina,
        this.strength,
        this.vision,
        this.volleys,
        this.weakFoot,
        this.traits,
        this.specialities,
        this.atkWorkRate,
        this.defWorkRate,
        this.playerType,
        this.attributes,
        this.name,
        this.rarityId,
        this.isIcon,
        this.quality,
        this.isGK,
        this.positionFull,
        this.isSpecialType,
        this.contracts,
        this.fitness,
        this.rawAttributeChemistryBonus,
        this.isLoan,
        this.squadPosition,
        this.iconAttributes,
        this.itemType,
        this.discardValue,
        this.id,
        this.modelName,
        this.baseId,
        this.rating});

  Players.fromJson(Map<String, dynamic> json) {
    commonName = json['commonName'];
    firstName = json['firstName'];
    lastName = json['lastName'];
    league =
    json['league'] != null ? new League.fromJson(json['league']) : null;
    nation =
    json['nation'] != null ? new Nation.fromJson(json['nation']) : null;
    club = json['club'] != null ? new Club.fromJson(json['club']) : null;
    headshot = json['headshot'] != null
        ? new Headshot.fromJson(json['headshot'])
        : null;
    position = json['position'];
    composure = json['composure'];
    playStyle = json['playStyle'];
    playStyleId = json['playStyleId'];
    height = json['height'];
    weight = json['weight'];
    birthdate = json['birthdate'];
    age = json['age'];
    acceleration = json['acceleration'];
    aggression = json['aggression'];
    agility = json['agility'];
    balance = json['balance'];
    ballcontrol = json['ballcontrol'];
    foot = json['foot'];
    skillMoves = json['skillMoves'];
    crossing = json['crossing'];
    curve = json['curve'];
    dribbling = json['dribbling'];
    finishing = json['finishing'];
    freekickaccuracy = json['freekickaccuracy'];
    gkdiving = json['gkdiving'];
    gkhandling = json['gkhandling'];
    gkkicking = json['gkkicking'];
    gkpositioning = json['gkpositioning'];
    gkreflexes = json['gkreflexes'];
    headingaccuracy = json['headingaccuracy'];
    interceptions = json['interceptions'];
    jumping = json['jumping'];
    longpassing = json['longpassing'];
    longshots = json['longshots'];
    marking = json['marking'];
    penalties = json['penalties'];
    positioning = json['positioning'];
    potential = json['potential'];
    reactions = json['reactions'];
    shortpassing = json['shortpassing'];
    shotpower = json['shotpower'];
    slidingtackle = json['slidingtackle'];
    sprintspeed = json['sprintspeed'];
    standingtackle = json['standingtackle'];
    stamina = json['stamina'];
    strength = json['strength'];
    vision = json['vision'];
    volleys = json['volleys'];
    weakFoot = json['weakFoot'];
    //traits = json['traits'].cast<String>();
    //specialities = json['specialities'].cast<String>();
    atkWorkRate = json['atkWorkRate'];
    defWorkRate = json['defWorkRate'];
    playerType = json['playerType'];
    if (json['attributes'] != null) {
      attributes = new List<Attributes>();
      json['attributes'].forEach((v) {
        attributes.add(new Attributes.fromJson(v));
      });
    }
    name = json['name'];
    rarityId = json['rarityId'];
    isIcon = json['isIcon'];
    quality = json['quality'];
    isGK = json['isGK'];
    positionFull = json['positionFull'];
    isSpecialType = json['isSpecialType'];
    contracts = json['contracts'];
    fitness = json['fitness'];
    rawAttributeChemistryBonus = json['rawAttributeChemistryBonus'];
    isLoan = json['isLoan'];
    squadPosition = json['squadPosition'];
    iconAttributes = json['iconAttributes'] != null
        ? new IconAttributes.fromJson(json['iconAttributes'])
        : null;
    itemType = json['itemType'];
    discardValue = json['discardValue'];
    id = json['id'];
    modelName = json['modelName'];
    baseId = json['baseId'];
    rating = json['rating'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['commonName'] = this.commonName;
    data['firstName'] = this.firstName;
    data['lastName'] = this.lastName;
    if (this.league != null) {
      data['league'] = this.league.toJson();
    }
    if (this.nation != null) {
      data['nation'] = this.nation.toJson();
    }
    if (this.club != null) {
      data['club'] = this.club.toJson();
    }
    if (this.headshot != null) {
      data['headshot'] = this.headshot.toJson();
    }
    data['position'] = this.position;
    data['composure'] = this.composure;
    data['playStyle'] = this.playStyle;
    data['playStyleId'] = this.playStyleId;
    data['height'] = this.height;
    data['weight'] = this.weight;
    data['birthdate'] = this.birthdate;
    data['age'] = this.age;
    data['acceleration'] = this.acceleration;
    data['aggression'] = this.aggression;
    data['agility'] = this.agility;
    data['balance'] = this.balance;
    data['ballcontrol'] = this.ballcontrol;
    data['foot'] = this.foot;
    data['skillMoves'] = this.skillMoves;
    data['crossing'] = this.crossing;
    data['curve'] = this.curve;
    data['dribbling'] = this.dribbling;
    data['finishing'] = this.finishing;
    data['freekickaccuracy'] = this.freekickaccuracy;
    data['gkdiving'] = this.gkdiving;
    data['gkhandling'] = this.gkhandling;
    data['gkkicking'] = this.gkkicking;
    data['gkpositioning'] = this.gkpositioning;
    data['gkreflexes'] = this.gkreflexes;
    data['headingaccuracy'] = this.headingaccuracy;
    data['interceptions'] = this.interceptions;
    data['jumping'] = this.jumping;
    data['longpassing'] = this.longpassing;
    data['longshots'] = this.longshots;
    data['marking'] = this.marking;
    data['penalties'] = this.penalties;
    data['positioning'] = this.positioning;
    data['potential'] = this.potential;
    data['reactions'] = this.reactions;
    data['shortpassing'] = this.shortpassing;
    data['shotpower'] = this.shotpower;
    data['slidingtackle'] = this.slidingtackle;
    data['sprintspeed'] = this.sprintspeed;
    data['standingtackle'] = this.standingtackle;
    data['stamina'] = this.stamina;
    data['strength'] = this.strength;
    data['vision'] = this.vision;
    data['volleys'] = this.volleys;
    data['weakFoot'] = this.weakFoot;
    data['traits'] = this.traits;
    data['specialities'] = this.specialities;
    data['atkWorkRate'] = this.atkWorkRate;
    data['defWorkRate'] = this.defWorkRate;
    data['playerType'] = this.playerType;
    if (this.attributes != null) {
      data['attributes'] = this.attributes.map((v) => v.toJson()).toList();
    }
    data['name'] = this.name;
    data['rarityId'] = this.rarityId;
    data['isIcon'] = this.isIcon;
    data['quality'] = this.quality;
    data['isGK'] = this.isGK;
    data['positionFull'] = this.positionFull;
    data['isSpecialType'] = this.isSpecialType;
    data['contracts'] = this.contracts;
    data['fitness'] = this.fitness;
    data['rawAttributeChemistryBonus'] = this.rawAttributeChemistryBonus;
    data['isLoan'] = this.isLoan;
    data['squadPosition'] = this.squadPosition;
    if (this.iconAttributes != null) {
      data['iconAttributes'] = this.iconAttributes.toJson();
    }
    data['itemType'] = this.itemType;
    data['discardValue'] = this.discardValue;
    data['id'] = this.id;
    data['modelName'] = this.modelName;
    data['baseId'] = this.baseId;
    data['rating'] = this.rating;
    return data;
  }
}

class League {
  LeagueImageUrls imageUrls;
  String abbrName;
  int id;
  Null imgUrl;
  String name;

  League({this.imageUrls, this.abbrName, this.id, this.imgUrl, this.name});

  League.fromJson(Map<String, dynamic> json) {
    imageUrls = json['imageUrls'] != null
        ? new LeagueImageUrls.fromJson(json['imageUrls'])
        : null;
    abbrName = json['abbrName'];
    id = json['id'];
    imgUrl = json['imgUrl'];
    name = json['name'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.imageUrls != null) {
      data['imageUrls'] = this.imageUrls.toJson();
    }
    data['abbrName'] = this.abbrName;
    data['id'] = this.id;
    data['imgUrl'] = this.imgUrl;
    data['name'] = this.name;
    return data;
  }
}

class LeagueImageUrls {
  String dark;
  String light;

  LeagueImageUrls({this.dark, this.light});

  LeagueImageUrls.fromJson(Map<String, dynamic> json) {
    dark = json['dark'];
    light = json['light'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['dark'] = this.dark;
    data['light'] = this.light;
    return data;
  }
}

class Nation {
  NationImageUrls imageUrls;
  String abbrName;
  int id;
  Null imgUrl;
  String name;

  Nation({this.imageUrls, this.abbrName, this.id, this.imgUrl, this.name});

  Nation.fromJson(Map<String, dynamic> json) {
    imageUrls = json['imageUrls'] != null
        ? new NationImageUrls.fromJson(json['imageUrls'])
        : null;
    abbrName = json['abbrName'];
    id = json['id'];
    imgUrl = json['imgUrl'];
    name = json['name'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.imageUrls != null) {
      data['imageUrls'] = this.imageUrls.toJson();
    }
    data['abbrName'] = this.abbrName;
    data['id'] = this.id;
    data['imgUrl'] = this.imgUrl;
    data['name'] = this.name;
    return data;
  }
}

class NationImageUrls {
  String small;
  String medium;
  String large;

  NationImageUrls({this.small, this.medium, this.large});

  NationImageUrls.fromJson(Map<String, dynamic> json) {
    small = json['small'];
    medium = json['medium'];
    large = json['large'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['small'] = this.small;
    data['medium'] = this.medium;
    data['large'] = this.large;
    return data;
  }
}

class Club {
  ImageUrls imageUrls;
  String abbrName;
  int id;
  Null imgUrl;
  String name;

  Club({this.imageUrls, this.abbrName, this.id, this.imgUrl, this.name});

  Club.fromJson(Map<String, dynamic> json) {
    imageUrls = json['imageUrls'] != null
        ? new ImageUrls.fromJson(json['imageUrls'])
        : null;
    abbrName = json['abbrName'];
    id = json['id'];
    imgUrl = json['imgUrl'];
    name = json['name'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.imageUrls != null) {
      data['imageUrls'] = this.imageUrls.toJson();
    }
    data['abbrName'] = this.abbrName;
    data['id'] = this.id;
    data['imgUrl'] = this.imgUrl;
    data['name'] = this.name;
    return data;
  }
}

class ImageUrls {
  Dark dark;
  Light light;

  ImageUrls({this.dark, this.light});

  ImageUrls.fromJson(Map<String, dynamic> json) {
    dark = json['dark'] != null ? new Dark.fromJson(json['dark']) : null;
    light = json['light'] != null ? new Light.fromJson(json['light']) : null;
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.dark != null) {
      data['dark'] = this.dark.toJson();
    }
    if (this.light != null) {
      data['light'] = this.light.toJson();
    }
    return data;
  }
}

class Dark {
  String small;
  String medium;
  String large;

  Dark({this.small, this.medium, this.large});

  Dark.fromJson(Map<String, dynamic> json) {
    small = json['small'];
    medium = json['medium'];
    large = json['large'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['small'] = this.small;
    data['medium'] = this.medium;
    data['large'] = this.large;
    return data;
  }
}

class Light {
  String small;
  String medium;
  String large;

  Light({this.small, this.medium, this.large});

  Light.fromJson(Map<String, dynamic> json) {
    small = json['small'];
    medium = json['medium'];
    large = json['large'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['small'] = this.small;
    data['medium'] = this.medium;
    data['large'] = this.large;
    return data;
  }
}

class Headshot {
  String imgUrl;
  bool isDynamicPortrait;

  Headshot({this.imgUrl, this.isDynamicPortrait});

  Headshot.fromJson(Map<String, dynamic> json) {
    imgUrl = json['imgUrl'];
    isDynamicPortrait = json['isDynamicPortrait'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['imgUrl'] = this.imgUrl;
    data['isDynamicPortrait'] = this.isDynamicPortrait;
    return data;
  }
}

class Attributes {
  String name;
  int value;
  List<int> chemistryBonus;

  Attributes({this.name, this.value, this.chemistryBonus});

  Attributes.fromJson(Map<String, dynamic> json) {
    name = json['name'];
    value = json['value'];
    chemistryBonus = json['chemistryBonus'].cast<int>();
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['name'] = this.name;
    data['value'] = this.value;
    data['chemistryBonus'] = this.chemistryBonus;
    return data;
  }
}

class IconAttributes {
  List<ClubTeamStats> clubTeamStats;
  List<NationalTeamStats> nationalTeamStats;
  String iconText;

  IconAttributes({this.clubTeamStats, this.nationalTeamStats, this.iconText});

  IconAttributes.fromJson(Map<String, dynamic> json) {
    if (json['clubTeamStats'] != null) {
      clubTeamStats = new List<ClubTeamStats>();
      json['clubTeamStats'].forEach((v) {
        clubTeamStats.add(new ClubTeamStats.fromJson(v));
      });
    }
    if (json['nationalTeamStats'] != null) {
      nationalTeamStats = new List<NationalTeamStats>();
      json['nationalTeamStats'].forEach((v) {
        nationalTeamStats.add(new NationalTeamStats.fromJson(v));
      });
    }
    iconText = json['iconText'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.clubTeamStats != null) {
      data['clubTeamStats'] =
          this.clubTeamStats.map((v) => v.toJson()).toList();
    }
    if (this.nationalTeamStats != null) {
      data['nationalTeamStats'] =
          this.nationalTeamStats.map((v) => v.toJson()).toList();
    }
    data['iconText'] = this.iconText;
    return data;
  }
}

class ClubTeamStats {
  int years;
  int clubId;
  String clubName;
  int appearances;
  int goals;

  ClubTeamStats(
      {this.years, this.clubId, this.clubName, this.appearances, this.goals});

  ClubTeamStats.fromJson(Map<String, dynamic> json) {
    years = json['years'];
    clubId = json['clubId'];
    clubName = json['clubName'];
    appearances = json['appearances'];
    goals = json['goals'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['years'] = this.years;
    data['clubId'] = this.clubId;
    data['clubName'] = this.clubName;
    data['appearances'] = this.appearances;
    data['goals'] = this.goals;
    return data;
  }
}

class NationalTeamStats {
  int years;
  int clubId;
  String clubName;
  int appearances;
  int goals;

  NationalTeamStats(
      {this.years, this.clubId, this.clubName, this.appearances, this.goals});

  NationalTeamStats.fromJson(Map<String, dynamic> json) {
    years = json['years'];
    clubId = json['clubId'];
    clubName = json['clubName'];
    appearances = json['appearances'];
    goals = json['goals'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['years'] = this.years;
    data['clubId'] = this.clubId;
    data['clubName'] = this.clubName;
    data['appearances'] = this.appearances;
    data['goals'] = this.goals;
    return data;
  }
}

Hey, this is a truncated version, get full version https://gist.github.com/piyushsinha24/f7da26ae1398321112cac0bbf822b12d

Data Provider

The player_api_provider is the lowest layer in our application architecture (the data provider). Its only responsibility is to fetch data directly from our API.

As we mentioned earlier, we are going to be hitting two endpoints so our player_api_provider needs to expose two public methods:

  • fetchPlayersByCountry(String countryId)
  • fetchPlayersByName(String name)

It should look something like this:

import 'dart:convert';

import 'package:flutter_bloc_example/models/api_models.dart';
import 'package:http/http.dart' as http;

class PlayerApiProvider {

  String baseUrl = "https://www.easports.com/fifa/ultimate-team/api/fut/item?";
  final successCode = 200;

  Future<List<Players>> fetchPlayersByCountry(String countryId) async {
    final response = await http.get(baseUrl + "country=" + countryId);

    return parseResponse(response);
  }

  Future<List<Players>> fetchPlayersByName(String name) async {
    final response = await http.get(baseUrl+"name="+name);

    return parseResponse(response);
  }

  List<Players> parseResponse(http.Response response) {
    final responseString = jsonDecode(response.body);

    if (response.statusCode == successCode) {
      return ApiResult.fromJson(responseString).items;
    } else {
      throw Exception('failed to load players');
    }
  }
}

Hey, this is a truncated version, get full version https://gist.github.com/piyushsinha24/03c94ecfd7df3997805e5caeadfd20f2

Repository

The repository.dart serves as an abstraction between the client code and the data provider so that as a developer working on features, you don’t have to know where the data is coming from. It may come from API provider or Local database. So a good practice is to use Repository pattern.

This is how it is done:

class PlayerRepository {
  PlayerApiProvider _playerApiProvider = PlayerApiProvider();

  Future<List<Players>> fetchPlayersByCountry(String countryId) =>
      _playerApiProvider.fetchPlayersByCountry(countryId);
  Future<List<Players>> fetchPlayersByName(String name) =>
      _playerApiProvider.fetchPlayersByName(name);
}

Awesome! We are now ready to move up to the business logic layer.

Business Logic (Bloc)

Our PlayerListingBloc is responsible for receiving PlayerListingEvents and converting them into PlayerListingStates. It will have a dependency on Repositoryso that it can retrieve the Players when a user taps on the country flag of their choice or types the name of the player in the search bar.

Before jumping into the Bloc we need to define what Events our PlayerListingBloc will be handling as well as how we are going to represent our States.

PlayerListingEvent

We have two events in this application:

  • CountrySelectedEvent : Whenever a user taps a country flag, we will dispatch a CountrySelectedEvent event with the given countryId and our bloc will responsible for figuring out what players are there in the team and returning a new State.
  • SearchTextChangedEvent : Whenever a user types the name of a player in the search bar, we will dispatch a SearchTextChangedEvent event with the given name and our bloc will responsible for figuring out what players are there in any of the country in the API with the matching name and returning a new State.

It should look something like this:

abstract class PlayerListingEvent{}

class CountrySelectedEvent extends PlayerListingEvent{
  final NationModel nationModel;
  CountrySelectedEvent({@required this.nationModel}) : assert(nationModel!=null);
}
class SearchTextChangedEvent extends PlayerListingEvent {
  final String searchTerm;
  SearchTextChangedEvent({@required this.searchTerm}) : assert(searchTerm != null);
}

PlayerListingStates

For the current application, we will have five possible states:

  • UninitialisedState - our initial state which will have no player data because the user has not fired any event.
  • FetchingState - a state which will occur while we are fetching the player data.
  • FetchedState - a state which will occur if we were able to successfully fetch player data.
  • ErrorState - a state which will occur if we were unable to fetch player data.
  • EmptyState - a state which will occur if we were able to fetch player data but found no match for the user request.

It should look something like this:

abstract class PlayerListingState {}
class PlayerUninitializedState extends PlayerListingState {}
class PlayerFetchingState extends PlayerListingState {}

class PlayerFetchedState extends PlayerListingState {
  final List<Players> players;
  PlayerFetchedState({@required this.players});
}
class PlayerErrorState extends PlayerListingState {}
class PlayerEmptyState extends PlayerListingState {}

Now that we have our Events and our States defined and implemented we are ready to make our PlayerListingBloc.

PlayerListingBloc

Our PlayerListingBloc is very straightforward. To recap, it converts PlayerListingEvents into PlayerListingStates and has a dependency on the Repository.

We set our initialState to UninitialisedState since initially, the user has not fired any event. Then, all that’s left is to implement mapEventToState.

If our event is CountrySelectedEvent then we will yield our FetchingState state and then try to get the player data from the Repository using fetchPlayersByCountry(String countryId) .

If our event is SearchTextChangedEvent then we will yield our FetchingState state and then try to get the player data from the Repository using fetchPlayersByName(String name) .

After successful fetching, if player data is null then we yield our EmptyState state else we yield our FetchedState state.

If the fetching failed due to some exception then we yield ourErrorState state.

See the code to understand the process:

class PlayerListingBloc extends Bloc<PlayerListingEvent, PlayerListingState> {
  final PlayerRepository playerRepository;
  PlayerListingBloc({this.playerRepository}) : assert(playerRepository != null);
   @override
  void onTransition(Transition<PlayerListingEvent, PlayerListingState> transition) {
    super.onTransition(transition);
    print(transition);
  }
  @override
  PlayerListingState get initialState => PlayerUninitializedState();

  @override
  Stream<PlayerListingState> mapEventToState(PlayerListingEvent event) async* {
     yield PlayerFetchingState();
      List<Players> players;
      try {
        if (event is CountrySelectedEvent) {
          players = await playerRepository
              .fetchPlayersByCountry(event.nationModel.countryId);
        } else if (event is SearchTextChangedEvent) {
          players = await playerRepository.fetchPlayersByName(event.searchTerm);
        }
        if (players.length == 0) {
          yield PlayerEmptyState();
        } else {
          yield PlayerFetchedState(players: players);
        }
      } catch (_) {
        yield PlayerErrorState();
      }
    }
  }

Hey, this is a truncated version, get full version https://gist.github.com/piyushsinha24/912a9bbc9e0aae8c678d41dac3db6f98

That’s all there is to it! Now we’re ready to move on to the final layer: the presentation layer.

Presentation

Our App widget is going to start off as a StatelessWidget which has the Repository injected and builds the MaterialApp with our widgets.

Our Home widget will be a StatefulWidget responsible for creating and disposing a PlayerListingBloc.

All that’s happening in this widget is we’re using BlocBuilder with our PlayerListingBloc in order to rebuild our UI based on state changes in our PlayerListingBloc.

Inside the scaffold, we have a column with three children widgets:

  • Horizontal Bar- contains a ListView.builder with horizontal scroll direction and has nation flags as items tapping on which dispatches CountrySelectedEvent .
  • Search Bar- contains a TextField and changing its content dispatches SearchTextChangedEvent.
  • Player Listing- checks for the current state of the application and returns a widget accordingly. For fetched state, it returns a ListView having ListTiles as children displaying player data and all wrapped with the Expanded widget. For Fetching state, it returns a CircularProgressIndicator. For other states, it returns a message.

From Home widget, tapping on any of the ListTiles will take us to PlayerProfile which is another Stateful widget which displays details specific to a player.

This blog wasn’t about UI/UX so I won’t be discussing about that in depth.

🎉 That’s all there is to it! We’ve now successfully implemented our app in flutter using the bloc and flutter_bloc packages and we’ve successfully separated our presentation layer from our business logic. I hope you enjoyed it, and leave a few claps 👏 if you did. 🎉

The full source for this example can be found here. If you find it useful please support me by ⭐️ the repository.

#flutter #dart #mobile-apps

How to implement BLoC pattern using flutter_bloc package
70.60 GEEK