Flutter App: fetching data from the API using the BLoC pattern architecture

Loredana Zdrânc
Zipper Studios
Published in
8 min readNov 12, 2019

Long title, no? That’s because this article is really complex. It is actually a step-by-step tutorial for building a weather app with Flutter, fetching data from a public API and demonstrating how to architect a Flutter app using the BLoC pattern. By the end of this article hopefully, you will be able to use reactive programming in Flutter, organize your app better and work with APIs.

Let’s get started with theory!

  • What is reactive programming and why should I use it?
  • How about the BLoC pattern, is it really useful?
  1. Reactive Programming

For me, reactive programming is the synonym of asynchronous work. We need asynchronous work to make our application more responsive and to improve the user experience without freezing the main thread. In reactive programming, data flows emitted by one component. The underlying structure provided by the Rx(Reactive Extensions) libraries will propagate those changes to another component that is registered to receive the data changes. In Flutter, we can implement reactive programming using the rxdart library. RxDart is based on Streams and Sinks. Streams represent the flux of data and events. With Streams, you can listen to data and event changes, and just as well, deal with what’s coming from the Stream with listeners. What about Sinks? If we have an output of a data flux, we also need an input and that’s what Sinks are used for. More details on how to implement that are presented in the practical section below.

2. The BLoC pattern

Created by Google, the BLoC (Business Logic Component) stands as a middleman between a source of data in your app (e.g an API response) and widgets that need the data. Its scope is to take handle the business logic code separately from the UI. In the image below you can see the data flux using BLoC pattern.

BLoC pattern

The multiple widgets from the app send data to the BLoC class through Sinks and they get notified when data is changed by Streams. There’s no business logic in the widget, which means that whatever happens in BLoC is not the concern of the UI. A concrete example will be shown below.

Let’s start coding! A weather app? Sounds interesting!

The idea of a weather app comes from a free public API that provides access to current weather data. We'll fetch the weather info from London city. I used the Android Studio IDE for building this app. Feel free to use the one you are familiar with.

Step 1. Create a new Flutter project with the command:

flutter create weather_app

or, in AndroidStudio with the following operations:

File -> New Flutter Project -> Flutter Application -> Configure the new Flutter application (don't forget to add the right Flutter SDK path).

Step 2. Add http, rxdart and intl dependencies in pubspec.yaml file as shown below. Http is used to make HTTP requests, rxdart provides the reactive programming implementation and intl is used for date and number formatting. Make sure you execute theflutter packages get command inside your project directory after you add these dependencies.

dependencies:
flutter:
sdk: flutter
http: ^0.12.0+2
rxdart: ^0.22.0
intl: ^0.15.8

Step 3. Organize your lib folder in subdirectories as shown below:

Step 4. Add models to your app. The API response is a JSON like this:

{  
"coord":{
"lon":-0.13,
"lat":51.51
},
"weather":[
{
"id":300,
"main":"Drizzle",
"description":"light intensity drizzle",
"icon":"09d"
}
],
"base":"stations",
"main":{
"temp":280.32,
"pressure":1012,
"humidity":81,
"temp_min":279.15,
"temp_max":281.15
},
"visibility":10000,
"wind":{
"speed":4.1,
"deg":80
},
"clouds":{
"all":90
},
"dt":1485789600,
"sys":{
"type":1,
"id":5091,
"message":0.0103,
"country":"GB",
"sunrise":1485762037,
"sunset":1485794875
},
"id":2643743,
"name":"London",
"cod":200
}

Create corresponding classes for this JSON. You can find the entire codebase on Github. For each class add the fromJson() method. This method handles the conversion of the JSON response into the models we define.

Step 5. Inside the persistence directory, create a Dart file called api_provider.dart. Inside it put the following code:

import 'dart:convert';
import 'package:http/http.dart' show Client;
import 'package:weather_app/model/weather_response_model.dart';

class ApiProvider {
Client client = Client();
final _baseUrl =
"https://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22";

Future<WeatherResponse> fetchLondonWeather() async {
final response = await client.get("$_baseUrl");
print(response.body.toString());

if (response.statusCode == 200) {
return WeatherResponse.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to load weather');
}
}
}

As its name says, the fetchLondonWeather() method makes the network call to the API to fetch the London weather data. This call returns a Future WeatherResponse object. A Future is used to represent a potential value, or error, that will be available at some time in the future. Receivers of a Future can register callbacks that handle the value or error once it is available. The response variable receives the call response and, depending on its status code, the weather response will be decoded or an exception will be thrown.

Step 6. Create a repository.dart file inside the same persistence directory.

import 'package:weather_app/model/weather_response_model.dart';
import 'api_provider.dart';

class Repository {
ApiProvider appApiProvider = ApiProvider();

Future<WeatherResponse> fetchLondonWeather() => appApiProvider.fetchLondonWeather();
}

This Repository class mediates between the domain and the data mapping layers, acting like an in-memory domain object collection. Maybe you are familiar with this class if you interacted with other programming languages.

Step 7. It’s BLoC time! The class responsible for displaying the weather to the user should have its own BLoC class that holds the business logic. Let’s call it WeatherBloc. Inside the bloc subdirectories, create a weather_bloc.dart file with the following code:

import 'package:rxdart/rxdart.dart';
import 'package:weather_app/model/weather_response_model.dart';
import 'package:weather_app/persistence/repository.dart';

class WeatherBloc {
Repository _repository = Repository();

final _weatherFetcher = PublishSubject<WeatherResponse>();

Observable<WeatherResponse> get weather => _weatherFetcher.stream;

fetchLondonWeather() async {
WeatherResponse weatherResponse = await _repository.fetchLondonWeather();
_weatherFetcher.sink.add(weatherResponse);
}

dispose() {
_weatherFetcher.close();
}
}

final weatherBloc = WeatherBloc();

Here the reactive programming concept makes its entrance. The WeatherBloc class contains a method used to call fetchLondonWeather() from the repository. The _weatherFetcher is a PublishSubject<WeatherResponse> object responsible for adding the data retrieved from the server in the form of a WeatherResponse object and passing it to the UI screen as a stream. To pass the WeatherResponse object as stream we have created another method weather whose return type is an Observable. Don't forget to close the PublicSubject object inside the dispose() method. The last line instantiates a WeatherBloc object, declared as final so that it can be accessed from the class responsible for displaying weather data. This is not the best approach, but in the next article, we will update the code using Dependency Injection to obtain a reference to the WeatherBloc object.

Step 8. We have the business logic for fetching weather data from the server. Let’s build the UI! Inside the ui directory create the weather_screen.dart file and start playing the most interesting Flutter game: the “Widgets” puzzle. Customize your screen as you want. I chose the simplest way possible:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:weather_app/bloc/weather_bloc.dart';
import 'package:weather_app/model/coord_model.dart';
import 'package:weather_app/model/main_model.dart';
import 'package:weather_app/model/sys_model.dart';
import 'package:weather_app/model/weather_response_model.dart';
import 'package:weather_app/model/wind_model.dart';

class WeatherScreen extends StatefulWidget {
@override
WeatherScreenState createState() => WeatherScreenState();
}

class WeatherScreenState extends State<WeatherScreen> {

@override
Widget build(BuildContext context) {
weatherBloc.fetchLondonWeather();
return StreamBuilder(
stream: weatherBloc.weather,
builder: (context, AsyncSnapshot<WeatherResponse> snapshot) {
if (snapshot.hasData) {
return _buildWeatherScreen(snapshot.data);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator());
});
}
Container _buildWeatherScreen(WeatherResponse data) {
return Container(
padding: const EdgeInsets.all(17.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buildTitle(data.name),
_buildCoord(data.coord),
_buildMain(data.main),
_buildWindInfo(data.wind),
_buildSys(data.sys),
],
),
);
}

Center _buildTitle(String name) {
return Center(
child: Text(
"Weather in " + name,
style:
TextStyle(color: Color.fromRGBO(0, 123, 174, 100), fontSize: 40.0),
textAlign: TextAlign.center,
),
);
}

Column _buildCoord(Coord coord) {
return Column(
children: <Widget>[
Container(
margin: const EdgeInsets.only(bottom: 12.0),
child: Text(
"Coord",
style: TextStyle(
color: Color.fromRGBO(0, 123, 174, 100), fontSize: 18.0),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text("Lat: " + coord.lat.toString()),
_buildVerticalDivider(),
Text("Lng: " + coord.lon.toString())
],
),
],
);
}

Column _buildMain(Main main) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
margin: const EdgeInsets.only(bottom: 12.0),
child: Text(
"Main",
style: TextStyle(
color: Color.fromRGBO(0, 123, 174, 100), fontSize: 18.0),
),
),
Text("Temperature: " + main.temp.toString()),
Text("Pressure: " + main.pressure.toString()),
Text("Humidity: " + main.humidity.toString()),
Text("Highest temperature: " + main.tempMax.toString()),
Text("Lowest temperature: " + main.tempMin.toString()),
],
);
}

Column _buildWindInfo(Wind wind) {
return Column(
children: <Widget>[
Container(
margin: const EdgeInsets.only(bottom: 12.0),
child: Text(
"Wind",
style: TextStyle(
color: Color.fromRGBO(0, 123, 174, 100), fontSize: 18.0),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text("Speed: " + wind.speed.toString()),
_buildVerticalDivider(),
Text("Degree: " + wind.deg.toString()),
],
)
],
);
}

Container _buildVerticalDivider() {
return Container(
height: 20, child: VerticalDivider(color: Colors.blueGrey));
}

Column _buildSys(Sys sys) {
final dateFormat = new DateFormat('hh:mm:ss');

var sunriseDate =
new DateTime.fromMillisecondsSinceEpoch(sys.sunrise * 1000);
var sunsetDate = new DateTime.fromMillisecondsSinceEpoch(sys.sunset * 1000);
return Column(
children: <Widget>[
Container(
margin: const EdgeInsets.only(bottom: 12.0),
child: Text(
"Sys",
style: TextStyle(
color: Color.fromRGBO(0, 123, 174, 100), fontSize: 18.0),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text("Sunrise: " + dateFormat.format(sunriseDate)),
_buildVerticalDivider(),
Text("Sunset: " + dateFormat.format(sunsetDate)),
],
),
],
);
}
}

Inside the build() method of the WeatherScreenState class we accessed the weatherBloc object and called the fetchLondonWeather() method. The WeatherBloc returns data as a stream. The incoming streams' listening and the updating of the UI is done with the help of a StreamBuilder widget. StreamBuilder is expecting a stream parameter where we pass the WeatherBloc weather method (since it is returning a stream). The moment there is a stream of data coming, StreamBuilder will re-render the widget with the latest data. Here, the snapshot data is holding the WeatherResponse object.

Step 10. Complete the main.dart file as below and run your project.

import 'package:bloc_pattern/ui/weather_screen.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Weather App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: WeatherScreen(),
);
}
}
Weather app

That’s all! You’ve just built your first architectured Flutter app with data fetched from API, using reactive programming and the BLoC pattern. The full code is available on Github.

This is just a first step, we can’t stop here, that’s why in the next article we will update this project using Dependency Injection to provide the ApiProvider, Repository, and WeatherBloc objects. I am waiting for your feedback or questions in the comments section below and, if you liked this tutorial, don't hesitate to give me some claps. Have a good day!

https://www.zipperstudios.co

Zipper Studios is a group of passionate engineers helping startups and well-established companies build their mobile products. Our clients are leaders in the fields of health and fitness, AI, and Machine Learning. We love to talk to likeminded people who want to innovate in the world of mobile so drop us a line here.

--

--