Architect your Flutter project using BLOC pattern
Hi Folks! I am back with another brand new article on Flutter. This time I will be talking and demonstrating you “how to architect your Flutter projects”. So that you can maintain, scale and test your Flutter projects easily. Before diving into the actual topic. I would like to share a small story on why we should focus on building a solid architect of our projects.
Why you need to architect your project?
“Once upon a time in the year 2015. I was a novice competitive programmer(Hackerearth profile) and was also learning android app development. As a competitive programmer I only cared about the output and efficiency of the program I wrote. I never gave a second thought to structuring my programs or projects I wrote. This trend and style reflected in my android projects as well. I was writing android apps with a competitive programmer’s mindset. At beginning when I was working on my own projects it was all good as I never had a boss or manager who can give me requirements to add new features or change existing features in the app. But when I started working in a startup and building android apps for them. I always took a lot of time to change an existing feature in the app. Not only that, I even added bugs as an add-on feature in the process of building the app. The main root cause of all these problems was, “I never followed any architectural pattern or never structured my projects”. As time passed and I started understanding the world of software, I transitioned myself from a competitive programmer to a software engineer. Today whenever I start a new project my primary focus is to build a solid structure or architecture of the project. That helped me to be a better and stress free software engineer 😄.”
Putting an end to my boring story 😅. Let’s start working on the main goal of this article. “Architect your Flutter projects using BLOC pattern”.
Our Goal
I will be building a very simple app. It will have one screen where you can see a grid list of items. Those items will be fetched from the server. The list of items will be popular movies from The Movies DB site.
Note : Before moving any further I assume you understand Widgets, how to make a network call in Flutter and have intermediate knowledge in Dart. This article will be little lengthy and heavily hyperlinked to other resources so that you can read further on specific topics.
So let’s begin the show. 😍
Before diving directly into the code. Let me give you a visual experience of the architecture we will be following to structure this app.
The BLOC pattern
The above diagram shows how the data flow from UI to the Data layer and vice versa. BLOC will never have any reference of the Widgets in the UI Screen. The UI screen will only observe changes coming from BLOC class. Let’s have a small Q&A to understand this diagram:
1. What is BLOC Pattern?
Its a state management system for Flutter recommended by Google developers. It helps in managing state and make access of data from a central place in your project.
2. Can I co-relate this architecture with any other architectures out there?
Yes of course. MVP and MVVM are some good example. Only the thing that will change is: BLOC will be replaced with ViewModel in MVVM.
3. What is under the hood of BLOC ? or What is that core thing that manages the state in one place ?
STREAMS or REACTIVE approach. In general terms, data will be flowing from the BLOC to the UI or from UI to the BLOC in the form of streams. If you have never heard about streams. Read this Stack Overflow answer.
Hope this small Q&A section cleared some of your doubts. If you need further clarity or want to ask a specific question, you can comment down below or connect directly with me at LinkedIn.
Let’s start building the project with BLOC pattern
- First create a new project and clear all the code in the main.dart file. Type below command in your terminal:
flutter create myProjectName
- Write down the below code in your main.dart file:
import 'package:flutter/material.dart';
import 'src/app.dart';
void main(){
runApp(App());
}
You must be getting an error in the second line. We will solve it in the upcoming steps.
- Create a src package under the lib package. Inside src package create a file and name it as app.dart. Copy paste the below code inside the app.dart file.
import 'package:flutter/material.dart';
import 'ui/movie_list.dart';
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build return MaterialApp(
theme: ThemeData.dark(),
home: Scaffold(
body: MovieList(),
),
);
}
}
- Create a new package inside the src package and name it as resources.
Now create few new packages i.e blocs, models, resources and ui as shown in the below diagram and then we are set with the skeleton of the project:
Project structure
blocs package will hold our BLOC implementation related files. models package will hold the POJO class or the model class of the JSON response we will be getting from the server. resources package will hold the repository class and the network call implemented class. ui package will hold our screens that will be visible to the user.
- One last thing we have to add i.e RxDart a third party library. Open your pubspec.yaml file and add rxdart : ^0.18.0 as shown below:
dependencies: flutter: sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
rxdart: ^0.18.0
sync your project or type below command in terminal. Make sure you execute this command inside your project directory.
flutter packages get
- Now we are complete with the skeleton of the project. Time to deal with the most bottom layer of the project i.e the network layer. Let’s understand the API end point which we are going to consume. Hit this link and you will be taken to the movie database API site. Sign up and get your API key from the Settings page. We will be hitting the below url to get the response:
http://api.themoviedb.org/3/movie/popular?api_key=“your_api_key”
Put your API key in the above link and hit(Remove double quotes as well). You can see the JSON response something like this:
{
"page": 1,
"total_results": 19772,
"total_pages": 989,
"results": [
{
"vote_count": 6503,
"id": 299536,
"video": false,
"vote_average": 8.3,
"title": "Avengers: Infinity War",
"popularity": 350.154,
"poster_path": "\/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg",
"original_language": "en",
"original_title": "Avengers: Infinity War",
"genre_ids": [
12,
878,
14,
28
],
"backdrop_path": "\/bOGkgRGdhrBYJSLpXaxhXVstddV.jpg",
"adult": false,
"overview": "As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his goal is to collect all six Infinity Stones, artifacts of unimaginable power, and use them to inflict his twisted will on all of reality. Everything the Avengers have fought for has led up to this moment - the fate of Earth and existence itself has never been more uncertain.",
"release_date": "2018-04-25"
},
- Let’s build a model or POJO class for this type of response. Create a new file inside the models package and name it as item_model.dart. Copy and paste the below code inside item_model.dart file:
class ItemModel {
int _page;
int _total_results;
int _total_pages;
List<_Result> _results = [];
ItemModel.fromJson(Map<String, dynamic> parsedJson) {
print(parsedJson['results'].length);
_page = parsedJson['page'];
_total_results = parsedJson['total_results'];
_total_pages = parsedJson['total_pages'];
List<_Result> temp = [];
for (int i = 0; i < parsedJson['results'].length; i++) {
_Result result = _Result(parsedJson['results'][i]);
temp.add(result);
}
_results = temp;
}
List<_Result> get results => _results;
int get total_pages => _total_pages;
int get total_results => _total_results;
int get page => _page;
}
class _Result {
int _vote_count;
int _id;
bool _video;
var _vote_average;
String _title;
double _popularity;
String _poster_path;
String _original_language;
String _original_title;
List<int> _genre_ids = [];
String _backdrop_path;
bool _adult;
String _overview;
String _release_date;
_Result(result) {
_vote_count = result['vote_count'];
_id = result['id'];
_video = result['video'];
_vote_average = result['vote_average'];
_title = result['title'];
_popularity = result['popularity'];
_poster_path = result['poster_path'];
_original_language = result['original_language'];
_original_title = result['original_title'];
for (int i = 0; i < result['genre_ids'].length; i++) {
_genre_ids.add(result['genre_ids'][i]);
}
_backdrop_path = result['backdrop_path'];
_adult = result['adult'];
_overview = result['overview'];
_release_date = result['release_date'];
}
String get release_date => _release_date;
String get overview => _overview;
bool get adult => _adult;
String get backdrop_path => _backdrop_path;
List<int> get genre_ids => _genre_ids;
String get original_title => _original_title;
String get original_language => _original_language;
String get poster_path => _poster_path;
double get popularity => _popularity;
String get title => _title;
double get vote_average => _vote_average;
bool get video => _video;
int get id => _id;
int get vote_count => _vote_count;
}
I hope you can map this file with the JSON response. If not, we are mostly interested in the poster_path inside the Results class that’s what you need to know to move further. We will display all the posters of the popular movies in our main UI. fromJson()
method is just getting the decoded json and putting the values in the correct variables.
- Now its time to work on the network implementation. Create a file inside the resources package and name it as movie_api_provider.dart. Copy and paste the below code inside the file and I will explain it to you:
import 'dart:async';
import 'package:http/http.dart' show Client;
import 'dart:convert';
import '../models/item_model.dart';
class MovieApiProvider {
Client client = Client();
final _apiKey = 'your_api_key';
Future<ItemModel> fetchMovieList() async {
print("entered");
final response = await client
.get("http://api.themoviedb.org/3/movie/popular?api_key=$_apiKey");
print(response.body.toString());
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return ItemModel.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
}
Note: Please put your API key in the variable _apiKey
inside the movie_api_provider.dart file or else it won’t work.
fetchMovieList()
method is making the network call to the API. Once the network call is complete its returning a Future ItemModel object if the network call was successful or it will throw an Exception.
- Next we are going to create a new file inside the resources package and name it as repository.dart. Copy and paste the below code inside the file:
import 'dart:async';
import 'movie_api_provider.dart';
import '../models/item_model.dart';
class Repository {
final moviesApiProvider = MovieApiProvider();
Future<ItemModel> fetchAllMovies() => moviesApiProvider.fetchMovieList();
}
We are importing the movie_api_provider.dar t file and calling its fetchMovieList()
method. This Repository class is the central point from where the data will flow to the BLOC.
- Now comes a little complicated part. Implementing the bloc logic. Let’s create a new file inside the blocs package and name it as movies_bloc.dart. Copy paste below code and I will explain you the code in little detail:
import '../resources/repository.dart';
import 'package:rxdart/rxdart.dart';
import '../models/item_model.dart';
class MoviesBloc {
final _repository = Repository();
final _moviesFetcher = PublishSubject<ItemModel>();
Observable<ItemModel> get allMovies => _moviesFetcher.stream;
fetchAllMovies() async {
ItemModel itemModel = await _repository.fetchAllMovies();
_moviesFetcher.sink.add(itemModel);
}
dispose() {
_moviesFetcher.close();
}
}
final bloc = MoviesBloc();
We are importing a package import ‘package:rxdart/rxdart.dart’;
which will eventually import all the RxDart related methods and classes in this file. Inside the MoviesBloc class we are creating the Repository class object which will be used to access the fetchAllMovies()
. We are creating a PublishSubject object whose responsibility is to add the data which it got from the server in form of ItemModel object and pass it to the UI screen as stream. To pass the ItemModel object as stream we have created another method allMovies()
whose return type is Observable(watch this video if you don’t understand Observables). If you see the last line we are creating the bloc object. This way we are giving access to a single instance of the MoviesBloc class to the UI screen.
If you don’t know what reactive programming is. Please read this simple explanation. In simple words, whenever there is new data coming from server. We have to update the UI screen. To make this updating task simple we are telling the UI screen to keep observing any change coming from the MoviesBloc class and accordingly update your content. This “observing” of new data can be doing using RxDart.
- Now the last section. Create a new file inside the ui package and name it as movie_list.dart. Copy paste the below code:
import 'package:flutter/material.dart';
import '../models/item_model.dart';
import '../blocs/movies_bloc.dart';
class MovieList extends StatelessWidget {
@override
Widget build(BuildContext context) {
//To keep it simple. I have done fetching of items here
bloc.fetchAllMovies();
return Scaffold(
appBar: AppBar(
title: Text('Popular Movies'),
),
body: StreamBuilder(
stream: bloc.allMovies,
builder: (context, AsyncSnapshot<ItemModel> snapshot) {
if (snapshot.hasData) {
return buildList(snapshot);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator());
},
),
);
}
Widget buildList(AsyncSnapshot<ItemModel> snapshot) {
return GridView.builder(
itemCount: snapshot.data.results.length,
gridDelegate:
new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (BuildContext context, int index) {
return Image.network(
'https://image.tmdb.org/t/p/w185${snapshot.data
.results[index].poster_path}',
fit: BoxFit.cover,
);
});
}
}
Best and interesting part of this class is, I am not using a StatefulWidget. But instead I am using a StreamBuilder which will do the same job what StatefulWidget does i.e updating the UI.
As I told you, our MoviesBloc class is passing the new data as stream. So to deal with streams we have a nice inbuilt class i.e StreamBuilder which will listen to the incoming streams and update the UI accordingly. StreamBuilder is expecting a stream
parameter where we are passing the MovieBloc’s allMovies()
method as it is returning a stream. So 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 ItemModel object. Now you can you any Widget to display whatever is there in the object(Here your creativity comes into the picture). I used a GridView to display all the posters that are there in the results list of ItemModel object. This is the output of the final product:
So we have hit the end of this article. Great job folks for holding on till the end. Hope you like this article. Please appreciate it with some claps and comments.