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:
A BLoC takes a stream of events as input and transforms them into a stream of states as output_._
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.
In flutter_bloc package we have :
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
is a Flutter widget which requires a Bloc
and a builder
function. BlocBuilder
handles building a widget in response to new states.
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.
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.Open https://www.easports.com/fifa/ultimate-team/api/fut/item?country=52 in your browser and you’ll see the following response:
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
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
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.
Our PlayerListingBloc
is responsible for receiving PlayerListingEvents
and converting them into PlayerListingStates
. It will have a dependency on Repository
so 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
.
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);
}
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
.
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.
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