Flutter Essential: Bloc, Networking (REST API call), Unit Testing, Code Coverage.

Everything you need to know in one tutorial.

Kustiawanto Halim
Flutter Community
Published in
10 min readDec 28, 2019

--

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:

  1. Project Setup
  2. REST API
  3. Data Model
  4. Data Provider
  5. Repository
  6. Business Logic (Bloc)
  7. Presentation
  8. Unit Testing
  9. Code Coverage
  10. 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

pubspec.yaml

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 == and hashCode 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.

quote.dart

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:

models.dart

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

quote_api_client.dart

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.

quote_repository.dart

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.

repository.dart

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:

quote_event.dart

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 no Quote have been fetched.
  • QuoteLoading, will occur when our app is fetching Quote.
  • QuoteLoaded, will occur when our app has successfully fetch a Quote.
  • QuoteError, will occur when our app unable to fetch Quote.

We represent the QuoteState as follows:

quote_state.dart

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.

quote_bloc.dart

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:

bloc.dart

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:

main.dart

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:

home_page.dart

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.

Screenshot of iPhone simulator displaying Quote app
Our App with HomePage Widget

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 add event being tested to the Bloc.
  • expect is an Iterable<State> that Bloc under test will emit after act 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_bloc_test.dart

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_api_client_test.dart

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_repository_test.dart

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:

quote_event_test.dart

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%

lcov HTML report

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:

References:

--

--

Kustiawanto Halim
Flutter Community

Mobile Apps Developer (Android, iOS, Flutter) | IoT enthusiast