Flutter Essential: Bloc, Networking (REST API call), Unit Testing, Code Coverage.
Everything you need to know in one tutorial.
Too many tutorials available on the internet, but still there is one problem that has always existed, that is, finding the most complete tutorial and discuss almost all topics that are needed to create a Flutter project. Now I give a complete article that can be used as your reference source.
So without further ado, let’s start making something. This article will discuss:
- Project Setup
- REST API
- Data Model
- Data Provider
- Repository
- Business Logic (Bloc)
- Presentation
- Unit Testing
- Code Coverage
- What’s next?
I strongly advise you to follow me along the steps.
1. Project Setup
Flutter version I used in this project:
Flutter 1.12.13+hotfix.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 27321ebbad
Engine • revision 2994f7e1e6
Tools • Dart 2.7.0
Let’s start creating a new flutter project
flutter create random_quote
and update pubspec.yaml
with some dependencies
lastly, don’t forget to install all dependencies
flutter packages get
2. REST API
We will hit the QuoteGarden for this project. If you want to know more about this API you can go straight to the Github repo here:
We will only use one endpoint, https://quote-garden.herokuapp.com/quotes/random to get a random quote and display it in our app. The response structure of the endpoint is simple JSON as follows:
{
"_id": "5d91b45d9980192a317c9b34",
"quoteText": "No one saves us but ourselves. No one can and no one may. We ourselves must walk the path.",
"quoteAuthor": "Buddha"
}
As you can see, we will get a JSON object, with 3 keys: id
, quoteText
, and quoteAuthor
. Now, we are ready to create our Data Model!
3. Data Model
We will have a really simple Data Model for our project. Of course, you can explore more API later on. Let’s start by creating a folder for our models in lib/models
and create a file called quote.dart
which will hold our data model for Quote
class.
Imports
First, we need to import our dependency on the class. At the very top of the file we will add:
import 'package:equatable/equatable.dart';
equatable
overrides==
andhashCode
for you so you don't have to waste your time writing lots of boilerplate code. Learn more here.
Create the Quote Model
Next, we need to create our defined data model for Quote
object returned from API. We are going to extract data from API, so our model should have id
, quoteText
and quoteAuthor
properties.
Now, let's implement three functions: props
, toString
, and fromJson
.
We need to override props
because we implement Equatable
in our data model class. Usually, we just need to return the data’s id, but we will just return every value.
fromJson
is a function to convert our JSON to Quote
object. We also override toString
function to make it readable.
Export in Barrel
Although we only have one data model for this project, but this is a best practice and a good habit to do. In case we want to scale our apps, exporting functions in barrel will help us in managing which files we need to expose and reduce dependencies import in our code.
Create a new file, lib/models/models.dart
as our barrel file and add the following code:
That’s all for our Data Model, now let’s move on to the next part.
4. Data Provider
Now, we will build our QuoteApiClient
that will responsible for making HTTP requests to our API. As the lowest layer in our app's architecture, QuoteApiClient
responsibility is only to fetch data directly from API. We only need to expose one public function which called fetchQuote
.
Creating QuoteApiClient
Let’s call this layer as repository
layer. Create a new file in lib/repositories
with name quote_api_client.dart
. Our class constructor should look like:
We are creating a private constant as our baseUrl
. The constructor will inject the HTTP client and instantiate it.
Fetch Quote Method
Now let’s implement the most important method to this class. fetchQuote
is an asynchronous
function that should return a Future
of Quote
, which is the data model object from JSON of our REST API response.
We use manual serialization of JSON using jsonDecode
method. This method will pass raw JSON string, look up the value, and return a Map<String, dynamic>
. Learn more about JSON and serialization here.
Imports
We need to add some missing dependencies in our quote_api_client.dart
dart:convert
package is Encoder/Decoder for converting between different data representations, including JSON and UTF-8.
meta
package defines annotations that can be used by the tools that are shipped with the Dart SDK. Learn more here.
http
package is a composable, Future-based library for making HTTP requests. Learn more here.
models
package is our barrel for all models
Final Code
5. Repository
We will complete our repository
layer, as we create the lowest layer, now we implement an abstraction layer that will be served our client code and data provider. Our QuoteRepository
will have QuoteApiClient
as it dependency and expose fetchQuote
method. In this layer, the client code doesn’t care how we get the Quote
data, but we only care about the Quote
data itself.
Quote Repository
Create a file named quote_repository.dart
under lib/repositories
.
Our QuoteRepository
will have QuoteApiClient
as it’s constructor. It only exposes one method, fetchQuote
that will call QuoteApiClient
fetchQuote
.
Barrel Export
Create a barrel file, named lib/repositories/repository.dart
and add the following line.
6. Business Logic (Bloc)
Now we have come to the most exciting part, to create the Bloc layer. Our QuoteBloc
will responsible to receive QuoteEvents
and convert them to QuoteStates
. It will have QuoteRepository
as its dependency so it can fetch Quote
.
Quote Event
The first Bloc we are going to create is our QuoteEvent
. Now create a file in lib/bloc
with name quote_event.dart
. We only have one event in our Bloc, FetchQuote
. Our FetchQuote
can be implemented as follows:
First, we create an abstract class named QuoteEvent
and we make it inherit Equatable
. Our event should inherit QuoteEvent
and we implement props
method in FetchQuote
.
We will add
ourFetchQuote
event when there is no Quote
that already fetched before, right after our initial state
. We will discuss this later in another section.
Quote State
Now, we can build our QuoteState
. Inside lib/bloc
we create a new file named quote_state.dart
. QuoteState
will define all possible states in our application. In our case, we have 4 possible states:
QuoteEmpty
, will be our initial state, where there is noQuote
have been fetched.QuoteLoading
, will occur when our app is fetchingQuote
.QuoteLoaded
, will occur when our app has successfully fetch aQuote
.QuoteError
, will occur when our app unable to fetchQuote
.
We represent the QuoteState
as follows:
We got an additional parameter for QuoteLoaded
because in this state
, our Bloc successfully fetch a Quote
and we will tell our views
to present the Quote
to user.
Quote Bloc
Our Bloc is straightforward, we want it to convert our QuoteEvent
to it’s, and has QuoteRepository
as its dependency.
QuoteBloc
constructor should inject QuoteRepository
since we need it to fetchQuote
. We also defined our initial state
as QuoteEmpty
. We override mapEventToState
method as we agreed before, whenever FetchQuote
event occurs, we yield
QuoteLoading
state and try to fetchQuote
using our repository. If we successfully fetched a Quote
, we yield
the QuoteLoaded
state, meanwhile if we failed, we yield
QuoteError
state.
Exporting the Barrel
To finish our Bloc implementation, we will expose all methods in our barrel file. Create a bloc.dart
file in lib/bloc
and add the following:
And that’s cover our Bloc implementation.
7. Presentation
Setup
Start by creating a Bloc delegate so we can see each state transition of our apps. In our main.dart
add the following code:
In each transition made between state, we just print
the transition so you can see it in terminal.
Now we just need to implement our delegate in main
method:
Still in our main
method, we create QuoteRepository
and inject it in our App
widget. Our main.dart
so far looks like:
App Widget
The App
widget will be a StatelessWidget
that has QuoteRepository
injected and build a MaterialApp
with HomePage
widget. We use BlocProvider
to create an instance of QuoteBloc
and manage it.
So our final main.dart
file will be like the following:
Home Page Widget
Our HomePage
widget is simple, let’s create a file in lib/views
with name home_page.dart
, and add the following code:
We are using BlocBuilder
to build our UI based on state in QuoteBloc
. We define 3 different UI that will be displayed based on different Bloc state
. When inQuoteEmpty
state, we add FetchQuote
event to our QuoteBloc
using BlocProvider
. If QuoteError
state occurs, we just show text failed to fetch quote
. Now, we will display a ListTile
with all successfully fetched Quote
data when QuoteLoaded
state occurs. We also display CircularProgressIndicator
as default UI for our states.
Run The App
To run our app, we can simply call this in terminal:
flutter run
Our App should look like this when finished running. I don’t polish the view, you can explore more in other tutorials :D
8. Unit Testing
Quote Bloc Test
Let’s start by creating test
folder and create a file named quote_bloc_test.dart
. In Flutter, we just add _test
suffix behind a file name to create the test file.
First, the basic one is to test our QuoteBloc
constructor. We assert that the constructor should inject QuoteRepository
and it should not be null
, and if error we should throw an AssertionError
. The test should look like below:
Before we can implement the rest of the test, we will use mockito
to define MockQuoteRepository
. By mocking the QuoteRepository
will allow us to simulate all its behavior.
mockito
is Mocking library in dart. Learn more here.
Now we create a MockQuoteRepository
class easily by:
class MockQuoteRepository extends Mock implements QuoteRepository {}
Next is to use setUp
method to instantiate our QuoteBloc
by injecting MockQuoteRepository
to it, and tearDown
to close
our Bloc because we don’t use regular BlocProvider
.
Our next test should check initialState
method. We override it in QuoteBloc
so it will return QuoteEmpty
state.
Related to close
behavior of our Bloc, we must ensure it doesn’t emit other new state
after close
.
The most important test is to simulate FetchQuote
event in our Bloc. First, we need to create a group
test inside our void main()
method.
group('Bloc test', () { // Bloc test here
}
We use bloc_test
library to help us test our bloc section efficiently. blocTest
will create a new specific bloc-test case with a given description. It will assert that the Bloc will emit the expected states
(in order) after specific act
is executed.
group('Bloc test', () { blocTest(
'test description',
build: () { // shoud return Bloc under test
},
act: (_) {},
expect: [ ],
);
}
The 3 main components of the blocTest
are:
build
where we prepare and instantiate Bloc under test.act
is an optional callback that will be invoked with the Bloc under test. It should be used to addevent
being tested to the Bloc.expect
is anIterable<State>
that Bloc under test will emit afteract
is executed.
Now let’s complete our first blocTest
, to make sure our Bloc emits [QuoteEmpty, QuoteLoading, QuoteLoaded]
when FetchQuote
event is added and fetchQuote
succeeds.
The second blocTest
is to make sure our Bloc emits [QuoteEmpty, QuoteLoading, QuoteError]
when FetchQuote
event is added and fetchQuote
unable to fetch a Quote
.
Our final quote_bloc_test.dart
should look like the following code:
Quote API Client Test
The next challenging part is to test our QuoteApiClient
. The very first thing to do is to create quote_api_client_test.dart
file under test
folder. Now by using mockito
we will createMockQuoteApiClient
and change the constructor so we can inject mock http.Client
to it.
And for our MockHttpClient
:
class MockHttpClient extends Mock implements http.Client {}
As we did before, let's create our assertion test:
We will test success and fail behavior of theQuoteApiClient
. Create a new group and instantiate our mock object.
The test will ensure QuoteApiClient
to return Quote
data model when successfully fetch it.
And for our second test, should expect that QuoteApiClient
throw an Exception
when it fails to fetch Quote
.
Our final quote_api_client_test.dart
should look like:
Quote Repository Test
After creating a unit test for our lowest layer, it is time to create a unit test for the repository layer. Start by creating quote_repository_test.dart
file, and build construction assertion tests.
QuoteRepository
have one expose method, fetchQuote
that will call fetchQuote
from QuoteApiClient
. Because QuoteRepository
have QuoteApiClient
as it’s dependency, we will create a MockQuoteApiClient
.
For the final quote_repository_test.dart
file will be:
Quote Event Test
To make sure we cover all the files, one more file that need to be tested is quote_event_test.dart
. The only method we need to cover is to make sure it emits the right props
. Our file will be:
Run the Tests!
To run the test, simply use:
flutter test
9. Code Coverage
To show code coverage of all our test, we can use:
flutter test --coverage
The above command will generate coverage reports using lcov
in coverage/lcov.info
. To convert these reports so we can read it, we need another tool.
In Ubuntu:
sudo apt-get update -qq -y
sudo apt-get install lcov -y
or in Mac:
brew install lcov
and to generate an HTML report of coverage test:
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
And our coverage report will be 100%
10. What’s next?
After this long tutorial, I hope you will give me some clap, and comments for improvements.
As for this project, you can use this as a starter project and scale it to be a bigger project.
And you can find the repo for this project here: