Bloc Pattern In Flutter || Part 2💙
In part 1, I gave a quick overview of how to use Bloc in our basic counter app. Now, it’s time for moving forward and learning a bit complex part. However, I am pretty sure you will understand it 😉
If Google made you directly land on this article and you didn’t checkout 1st part, here’s the link:
What did we learn in the 1st part? 🤔
- What is Bloc?
- What are Events and States?
- How does the stream works?
- What is BlocProvider, BlocBuilder and BlocConsumer? (Going to refresh your memory on this though 😉)
In this part, we will implement a Weather App using Bloc Pattern. You will also get an insight into how to use the http package for API Calling🥳
So, let’s get started!!!!!
Packages that we will use:
- equatable:
Equatable
overrides==
andhashCode
for you so you don't have to waste your time writing lots of boilerplate code. - flutter_bloc:
flutter_bloc
is our main package that is made to make our task easy to implement bloc in Flutter. It is made to work with the official package: bloc - http: This package contains a set of high-level functions and classes that make it easy to consume HTTP resources. It’s multi-platform and supports mobile, desktop, and browser. So, mainly we will use this package for the HTTP Requests.
In a Bloc, there will be 3 main files:
- bloc file: This file contains the main Business Logic
- events file: This file states all the events that are present in your app.
- state file: This file contains all the states that your app undergoes.
flutter_bloc package provides 4 main classes that we can wrap around a widget:
- BlocProvider: BlocProvider is the class/widget that will provide the instance of Bloc to the child subtree. In most cases,
BlocProvider
should be used to create new blocs which will be made available to the rest of the subtree. In this case, sinceBlocProvider
is responsible for creating the bloc, it will automatically handle closing it. Syntax:
BlocProvider(
create: (BuildContext context) => BlocA(),
child: ChildA(),
);
- BlocBuilder: BlocBuilder widget is responsible for building/re-building the child subtree whenever there is a state change. It requires a
bloc
and abuilder
function.BlocBuilder
is very similar toStreamBuilder
but has a more simple API to reduce the amount of boilerplate code needed. Syntax:
BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
// return widget here based on BlocA's state
}
)
- BlocListener: BlocListener is a Flutter widget that takes a
BlocWidgetListener
and an optionalbloc
and invokes thelistener
in response to state changes in the bloc. It should be used for functionality that needs to occur once per state change such as navigation, showing aSnackBar
, showing aDialog
, etc...listener
is only called once for each state change (NOT including the initial state) unlikebuilder
inBlocBuilder
and is avoid
function. Syntax:
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: Container(),
)
- BlocConsumer: BlocConsumer is a mixture of BlocBuilder and BlocListener. It exposes a
builder
andlistener
to react to new states.BlocConsumer
is analogous to a nestedBlocListener
andBlocBuilder
but reduces the amount of boilerplate needed.BlocConsumer
should only be used when it is necessary to both rebuild UI and execute other reactions to state changes in thebloc
. Syntax:
BlocConsumer<BlocA, BlocAState>(
listener: (context, state) {
// do stuff here based on BlocA's state
},
builder: (context, state) {
// return widget here based on BlocA's state
}
)
Too much theory done, isn’t it? So now, let’s do some real stuff!!
Step 0: Before we start, this time we will create a better project architecture by separating bloc, model, screens and services in our project. You can create this folder initially too. After completing, this is how our project structure will look:
Step 1: Import equatable, flutter_bloc and http in pubspecy.yaml
equatable: ^1.2.5
flutter_bloc: ^6.1.1
http: ^0.12.2
Step 2: Create a folder named bloc
and create 3 files in it. Here, we will name them weather_bloc
,weather_event
and weather_state
.
Step 3: In this tutorial, we are going to use a fake Weather API. You can also use any real one whichever you prefer.
Base URL: https://01b0f7a1-9563-47f5-b628-f14c51464ec5.mock.pstmn.io/api/get-weather-success?city=
Step 4: Let’s first check out the response of the API and create a model for the same. We will use this model when we get a response from our API. The above API gives the following response:
As this is a fake API, every city name will give the same response 🤣
Now, create a file for the model as lib/model/weather.dart. The model file will look like this:
class Weather {
int temperature;
int yesterdayMax;
int yesterdayMin;
int todayMax;
int todayMin;
String city;
Weather({
this.city,
this.temperature,
this.yesterdayMax,
this.yesterdayMin,
this.todayMax,
this.todayMin,
});
}
Step 5: Next step is to create our ApiService and then move ahead. To start with, first create a folder named services inside lib folder i.e. lib/services. Inside services folder, create a file named api_service.dart. Your file structure will look like:
Inside api_service.dart you can write the API Request code using http package. Here’s the code for the same:
import 'dart:convert';
import 'package:bloc_weather_example/screens/constants.dart';
import 'package:bloc_weather_example/model/weather.dart';
import 'package:http/http.dart' as http;class ApiService {
static Future getWeather(String city) async {
String requestUrl = https://01b0f7a1-9563-47f5-b628-f14c51464ec5.mock.pstmn.io/api/get-weather-success?city=$city; http.Response response = await http.get(requestUrl);
if (response.statusCode == 200) {
var decodedData = jsonDecode(response.body);
Weather _weather = Weather(
city: city,
temperature: decodedData['temperature'],
yesterdayMax: decodedData['yesterdayMax'],
yesterdayMin: decodedData['yesterdayMin'],
todayMax: decodedData['todayMax'],
todayMin: decodedData['todayMin']);
return _weather;
} else {
print(response.statusCode);
}
}
}
Here, we have created a method called getWeather() that takes city name as a parameter. We then append the baseUrl with the city name. This will become our requestUrl. The next step is to create a get request to the requestUrl.
We will get a Response object as the response of the get request. We now have to decode it so that we can extract its values. Hence, if we successfully get the response (status code == 200) we decode the response object using a method called jsonDecode() and passing response.body to it. Once we have decoded the response, we can create an object of our model and return it 🥳
Step 6: Now, let’s create our bloc files. Create a folder named bloc
(lib/bloc)and create 3 files in it. Here, we will name them as weather_bloc
, weather_event
and weather_state
. It will look like this:
weather_event.dart:
part of 'weather_bloc.dart';@immutable
abstract class WeatherEvent {}class WeatherInitialEvent extends WeatherEvent {}class WeatherGetEvent extends WeatherEvent {
final String city;
WeatherGetEvent({this.city});
}
Here, we have created only 2 events i.e. WeatherInitialEvent and WeatherGetEvent. The WeatherGetEvent takes city name as parameter. Spoiler Alert: This city name will be passed to our ApiService method from bloc😉
weather_state.dart
part of 'weather_bloc.dart';@immutable
abstract class WeatherState {}class WeatherInitial extends WeatherState {}class WeatherLoadingState extends WeatherState {}class WeatherGetState extends WeatherState {}class WeatherLoadedState extends WeatherState {
final Weather weather;
WeatherLoadedState({this.weather});
}
Here, we have created 4 states. WeatherInitial, WeatherLoading, WeatherGetState and WeatherLoadedState. The states will be doing the work as per their names. The WeatherLoadedState takes up a parameter of our model Weather which we will use in our UI.
weather_bloc.dart:
import 'dart:async';import 'package:bloc/bloc.dart';
import 'package:bloc_weather_example/model/weather.dart';
import 'package:bloc_weather_example/services/api_service.dart';
import 'package:meta/meta.dart';part 'weather_event.dart';
part 'weather_state.dart';class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
WeatherBloc() : super(WeatherInitial());@override
Stream<WeatherState> mapEventToState(WeatherEvent event) async* {
if (event is WeatherGetEvent) {
yield WeatherLoadingState();
Weather _weather = await ApiService.getWeather(event.city);
yield WeatherLoadedState(weather: _weather);
}
}
}
In our bloc file, we are just calling the ApiService.getWeather() method. Before the call, we yield the loading state to show the CircularProgressIndicator. Once the response is available, we yield a loaded state and pass the model response to the state.
So, here we have completed our bloc implementation. So, now let’s implement our UI!! 😍
Step 7: Create a folder lib/screens where we will store all our UI related files. Let’s first update main.dart:
import 'package:bloc_weather_example/bloc/weather_bloc.dart';
import 'package:bloc_weather_example/screens/home.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
debugShowCheckedModeBanner: false,
home: BlocProvider(
create: (context) => WeatherBloc(),
child: Home(),
),
);
}
}
Step 8: Now, let’s create home.dart which contains our main UI.
home.dart:
import 'package:bloc_weather_example/bloc/weather_bloc.dart';
import 'package:bloc_weather_example/screens/weather_card.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';class Home extends StatefulWidget {
Home({Key key}) : super(key: key);@override
_HomeState createState() => _HomeState();
}class _HomeState extends State<Home> {
TextEditingController _city = TextEditingController();
@override
Widget build(BuildContext context) {
return Stack(
children: [
Image.asset(
'assets/bg.jpg',
fit: BoxFit.cover,
),
Scaffold(
backgroundColor: Colors.transparent,
body: BlocBuilder<WeatherBloc, WeatherState>(
builder: (BuildContext context, state) {
if (state is WeatherLoadingState) {
print('Weather Loading State');
return Center(
child: CircularProgressIndicator(),
);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
state is WeatherLoadedState
? WeatherCard(
state: state,
)
: Container(),
SizedBox(
height: 30.0,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _city,
style: TextStyle(color: Colors.black, fontSize: 20.0),
decoration: InputDecoration(
fillColor: Colors.white70,
filled: true,
hintText: 'Enter City Name',
hintStyle: TextStyle(color: Colors.black26),
prefixIcon: Icon(Icons.home),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(20.0),
),
borderSide: BorderSide(
color: Colors.white,
width: 2.0,
),
),
),
keyboardType: TextInputType.name,
),
),
SizedBox(
height: 20.0,
),
Container(
height: 50.0,
width: 200.0,
child: RaisedButton(
child: Text(
'Check Weather!',
style: TextStyle(color: Colors.white),
),
color: Colors.blue.shade400,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(20.0),
),
),
onPressed: () async {
context.read<WeatherBloc>().add(
WeatherGetEvent(city: _city.text),
);
},
),
),
],
),
);
},
),
),
],
);
}
}
Here, we have wrapped our enter UI with BlocBuilder. Now, if the state is WeatherLoadingState, we show CircularProgressIndicator(). Otherwise, we show the entire UI. Once the WeatherLoadedState is obtained, we show a custom WeatherCard to display the details. The other main thing is the onPressed of our button. When the user presses the button, we fire WeatherGetEvent as follows:
context.read<WeatherBloc>().add(WeatherGetEvent(city: _city.text));
The above piece of code reads the context of WeatherBloc which we had passed from the main.dart i.e. the parent of this widget and then adds the event. Alternatively, we can first initialize our WeatherBloc object in initState() and then add the event in onPressed.
Step 9: Now, let’s create our custom WeatherCard. Create a file named weather_card.dart inside lib/screens. You can always have your own UI.
weather_card.dart:
import 'package:bloc_weather_example/bloc/weather_bloc.dart';
import 'package:bloc_weather_example/screens/constants.dart';
import 'package:flutter/material.dart';class WeatherCard extends StatelessWidget {
final WeatherLoadedState state;
WeatherCard({this.state});
@override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(
side: new BorderSide(color: Colors.blue, width: 3.0),
borderRadius: BorderRadius.circular(10.0),
),
color: Colors.white60,
elevation: 10.0,
shadowColor: Colors.yellow,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(
'City: ' + state.weather.city.toString() + ' 🏡',
style: weatherCardTextStyle,
),
SizedBox(
height: 10.0,
),
Text(
'Current Temperature: ' + state.weather.temperature.toString(),
style: weatherCardTextStyle,
),
SizedBox(
height: 10.0,
),
Text(
'Today\'s Maximum: ' + state.weather.todayMax.toString(),
style: weatherCardTextStyle,
),
SizedBox(
height: 10.0,
),
Text(
'Today\'s Minimum: ' + state.weather.todayMin.toString(),
style: weatherCardTextStyle,
),
SizedBox(
height: 10.0,
),
Text(
'Yesterday\'s Maximum: ' + state.weather.yesterdayMax.toString(),
style: weatherCardTextStyle,
),
SizedBox(
height: 10.0,
),
Text(
'Yesterday\'s Minimum: ' + state.weather.yesterdayMin.toString(),
style: weatherCardTextStyle,
),
SizedBox(
height: 10.0,
),
],
),
),
);
}
}
Here, we can access the parameters obtained from API using our state object. I have used a custom style weatherCardTextStyle and placed it under lib/screens/constants.dart. This is a good practice as the same style is being used everywhere.
TextStyle weatherCardTextStyle = TextStyle(
color: Colors.black,
fontSize: 20.0,
);
Step 10: Run your app 😍
We just implemented Bloc in our Weather Application💙
If you directly landed on this article, here’s part one where a basic counter example was implemented:
Bloc Pattern in Flutter | Part 1💙 | by Abhishek Doshi | Medium
Hope you enjoyed this article!
GitHub Repository: https://github.com/AbhishekDoshi26/bloc_weather_example
If you loved it, you can Buy Me A Coffee!
Don’t stop, until you are breathing!💙
- Abhishek Doshi