Flutter Series - Fetch data from the internet
Tags: flutter, dart, android, ios
Published: 2021-06-05 16:03:24

In the last tutorial, we built our first flutter app. In that app we displayed random country names. We hardcoded those names in the app. 
{% link https://dev.to/tanweer919/flutter-series-create-your-first-app-in-flutter-jdd %}
In this tutorial, we will make this app dynamic by fetching the country date from a real API. But before that we have to make some improvement in the app.
## Improving UI
First of all, we need to improve our UI a little bit. We use `Card` widget to display each country detail but we can improve the look by replacing the `Card` widget by a `Container`. To give the `Container` a raised look, we will use `BoxShadow` and to give corner a rounded look, we will use use `BorderRadius`. `Container` has a `boxDecoration` property to improve appearance of a `Container`. `boxDecoration` takes a `BoxDecoration` widget. In `BoxDecoraion` we can assign the boxShadow and borderRadius. 
Here is the **CountryList** class:
```dart
class CountryList extends StatelessWidget {
  final List<String> countries = [ "Algeria", "Angola", "Benin", "Botswana", "Burkina Faso", "Burundi", "Cabo Verde", "Cameroon", "Chad", 
  "Comoros ", "Congo (the)", "Côte d'Ivoire", "Djibouti", "Egypt", "Equatorial Guinea", "Eritrea", "Ethiopia", 
  "Gabon", "Gambia ", "Ghana", "Guinea", "Guinea-Bissau", "Kenya", "Lesotho", "Liberia", "Libya", "Madagascar", "Malawi", "Mali"];
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: countries.length,
      itemBuilder: (BuildContext context, int index) => Padding(
        padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.all(Radius.circular(10)),
            boxShadow: [
              BoxShadow(
                color: Colors.grey.withOpacity(0.2),
                spreadRadius: 2,
                blurRadius: 3,
                offset:
                    Offset(0, 3), // changes position of shadow
              ),
            ],
          ),
          height: 70,
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                CircleAvatar(
                  backgroundColor: Color(
                    (Random().nextDouble() * 0xFFFFFF).toInt(),
                  ).withOpacity(1.0),
                  child: Text(
                    countries[index].substring(0, 2),
                    style: TextStyle(color: Colors.white),
                  ),
                ),
                Expanded(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 8.0),
                    child: Text(
                      countries[index],
                      style: TextStyle(fontSize: 18),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
```
![Screenshot 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hvu0b9fg84wdf5g7bfzc.jpeg)

## Improving structure of the project
Our project at the moment is very small with just a single custom widget. So our whole code lives in a single file ie. **main.dart**. But once the project gets larger, we have to follow some pattern of breaking down the project into smaller parts or units to make the code more maintainable.
For now, we will create a **screens/** directory in **lib/**. This directory will contain individual screen/page of the app. Currently we have only one page ie.Homepage. In **screens/** directory create **HomeScreen.dart** file to contain HomePage code.
Move the `HomePage` and `CountryList` widget into the **HomeScreen.dart** file. Import **flutter/material.dart**(for all the material widgets and classes) and **dart:math**(for Random function) package in this file. **HomeScreen.dart** will now look like this.
```dart
import 'package:flutter/material.dart';
import 'dart:math';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Country List')
      ),
      body: CountryList(),
    );
  }
}

class CountryList extends StatelessWidget {
  final List<String> countries = [ "Algeria", "Angola", "Benin", "Botswana", "Burkina Faso", "Burundi", "Cabo Verde", "Cameroon", "Chad", 
  "Comoros ", "Congo (the)", "Côte d'Ivoire", "Djibouti", "Egypt", "Equatorial Guinea", "Eritrea", "Ethiopia", 
  "Gabon", "Gambia ", "Ghana", "Guinea", "Guinea-Bissau", "Kenya", "Lesotho", "Liberia", "Libya", "Madagascar", "Malawi", "Mali"];
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: countries.length,
      itemBuilder: (BuildContext context, int index) => Padding(
        padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.all(Radius.circular(10)),
            boxShadow: [
              BoxShadow(
                color: Colors.grey.withOpacity(0.2),
                spreadRadius: 2,
                blurRadius: 3,
                offset:
                    Offset(0, 3), // changes position of shadow
              ),
            ],
          ),
          height: 70,
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                CircleAvatar(
                  backgroundColor: Color(
                    (Random().nextDouble() * 0xFFFFFF).toInt(),
                  ).withOpacity(1.0),
                  child: Text(
                    countries[index].substring(0, 2),
                    style: TextStyle(color: Colors.white),
                  ),
                ),
                Expanded(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 8.0),
                    child: Text(
                      countries[index],
                      style: TextStyle(fontSize: 18),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
```
Since **main.dart** file make reference to `HomePage` widget, so we need to import `HomePage` widget in the **main.dart** file. This can be done by importing **HomeScreen.dart** file into the **main.dart** file. We can provide both relative as well as absolute path during import.
Relative path:
`import 'screens/HomeScreen.dart';`
Absolute path:
`import 'package:country_list/screens/HomeScreen.dart';`
We will use relative paths for user created files.
So now **main.dart** looks like this:
```dart
import 'package:flutter/material.dart';
import 'screens/HomeScreen.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    ),
  );
}
```

## Extracting large widgets
To make code even more maintainable, we are going to extract large widgets within a screen into its own file. These files will be created in **widgets/** directory in **lib/**. For this project, we are going to extract `CountryList` widget into **CountryList.dart** file in **widgets/**.

**widgets/CountryList.dart**
```dart
import 'package:flutter/material.dart';
import 'dart:math';
class CountryList extends StatelessWidget {
  final List<String> countries = [ "Algeria", "Angola", "Benin", "Botswana", "Burkina Faso", "Burundi", "Cabo Verde", "Cameroon", "Chad",
    "Comoros ", "Congo (the)", "Côte d'Ivoire", "Djibouti", "Egypt", "Equatorial Guinea", "Eritrea", "Ethiopia",
    "Gabon", "Gambia ", "Ghana", "Guinea", "Guinea-Bissau", "Kenya", "Lesotho", "Liberia", "Libya", "Madagascar", "Malawi", "Mali"];
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: countries.length,
      itemBuilder: (BuildContext context, int index) => Padding(
        padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.all(Radius.circular(10)),
            boxShadow: [
              BoxShadow(
                color: Colors.grey.withOpacity(0.2),
                spreadRadius: 2,
                blurRadius: 3,
                offset:
                Offset(0, 3), // changes position of shadow
              ),
            ],
          ),
          height: 70,
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                CircleAvatar(
                  backgroundColor: Color(
                    (Random().nextDouble() * 0xFFFFFF).toInt(),
                  ).withOpacity(1.0),
                  child: Text(
                    countries[index].substring(0, 2),
                    style: TextStyle(color: Colors.white),
                  ),
                ),
                Expanded(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 8.0),
                    child: Text(
                      countries[index],
                      style: TextStyle(fontSize: 18),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
```
Now we need to import this widget in **HomeScreen.dart**.

**screens/HomeScreen.dart**
```dart
import 'package:flutter/material.dart';
import '../widgets/CountryList.dart';
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Country List')
      ),
      body: CountryList(),
    );
  }
}
```

## Stateful Widget
As we already have discussed in the last article that there are two types of widget. Widgets with state and widget without state. We have already learned about the first type ie. Stateless Widget. Now we are going to learn about the second type, Stateful Widget. We can create Stateful widget by extending the StatefulWidget class. We can convert our `CountryList` widget into Stateful widget.
Now `CountryList` widget will look like this:
```dart
import 'package:flutter/material.dart';
import 'dart:math';
class CountryList extends StatefulWidget {
  @override
  _CountryListState createState() => _CountryListState();
}

class _CountryListState extends State<CountryList> {
  final List<String> countries = [ "Algeria", "Angola", "Benin", "Botswana", "Burkina Faso", "Burundi", "Cabo Verde", "Cameroon", "Chad",
    "Comoros ", "Congo (the)", "Côte d'Ivoire", "Djibouti", "Egypt", "Equatorial Guinea", "Eritrea", "Ethiopia",
    "Gabon", "Gambia ", "Ghana", "Guinea", "Guinea-Bissau", "Kenya", "Lesotho", "Liberia", "Libya", "Madagascar", "Malawi", "Mali"];

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: countries.length,
      itemBuilder: (BuildContext context, int index) => Padding(
        padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.all(Radius.circular(10)),
            boxShadow: [
              BoxShadow(
                color: Colors.grey.withOpacity(0.2),
                spreadRadius: 2,
                blurRadius: 3,
                offset:
                Offset(0, 3), // changes position of shadow
              ),
            ],
          ),
          height: 70,
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                CircleAvatar(
                  backgroundColor: Color(
                    (Random().nextDouble() * 0xFFFFFF).toInt(),
                  ).withOpacity(1.0),
                  child: Text(
                    countries[index].substring(0, 2),
                    style: TextStyle(color: Colors.white),
                  ),
                ),
                Expanded(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 8.0),
                    child: Text(
                      countries[index],
                      style: TextStyle(fontSize: 18),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
```
We can see `CountryList` extends `StatefulWidget`. But `StatefulWidget` does not contain build method. It just contains different states of a widget. For this we need to create state for this widget. This is done in the line `_CountryListState createState() => _CountryListState();`.
`createState` method returns a widget which contains individual state of this widget. `() =>` is fat arrow notation of defining a function which just have a return statement. 

`_CountryListState` is the widget containing individual state of `CountryList` since it extends `State<CountryList>`. **_** before name in dart is used to make a variable, class or method private to its file. So `_CountryListState` is private to the file and can't be accessed from outside this file. This `CountryList` will contain all the state variables that can change. 

**setState**
Now question arises, how can we change state? State change can be done by calling `setState` method with new state. `setState` takes an anonymous function. And within that function we can assign new state. Example:
```dart
Class _ExampleWidgetState extends State<ExampleWidget> {
  int count = 1;
  @override
  Widget build(BuildContext build) {
    return Center(
      child: ElevatedButton(
        onPressed: () {
          setState(() {
            count += 1;
          });
        },
        child: const Text('Count $count),
      ),
    );
  }
}
```

## API for country data
We are going to fetch data from an API using HTTP requests. The url of the API:
> https://api.first.org/data/v1/countries

This API will return following json:
```json
{
status: "OK",
status-code: 200,
version: "1.0",
total: 251,
limit: 100,
offset: 0,
access: "public",
data: {
  DZ: {
    country: "Algeria",
    region: "Africa"
  },
  AO: {
    country: "Angola",
    region: "Africa"
  },
  ...
}
```

We are concerned with data property. It is an object containing country code as its keys and object containing country details as its respective value.

## Installing an external package
Flutter allows installation of external packages from [pub.dev](https://pub.dev/). This packages allows quickly building an app without having to develop everything from scratch.
For this project, we are going to install [dio](https://pub.dev/packages/dio). This package provides a powerful http client which helps in making and hadling http requests. To install dio, open **pubspec.yaml** present in the root of the project and under `dependencies:` below `cupertino_icons: ^1.0.2` write this line
> dio: ^4.0.0

Here `dio` indicates package name while `^1.0.2` indicates version to be installed.
After that run `flutter pub get` to install all the packages.

## Model the country data
Since we have three property related to each country ie. country code, country name and continent of each country. We need to store all three property of each country in a single variable/unit. Best way is to create a **model** for each country. Model is nothing but a dart class used to store data.
Create a **models/** directory in **lib/**. This directory will contain all the models created in the project. 
Inside **models/**, create a **Country.dart** file. Content of the file will be following:
```dart
import 'package:flutter/cupertino.dart';
import 'dart:math' show Random;

class Country {
  String name;
  String region;
  String code;
  Color backgroundColor;
  Country(
      {this.name,
      this.region,
      this.code,
      this.backgroundColor});
  Country.fromJson({String countryCode, Map<String, dynamic> data})
      : this.code = countryCode,
        this.name = data["country"],
        this.region = data["region"],
        this.backgroundColor =
            Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
}
```
Here `Country` class represent the model with four properties. We have moved `backgroundColor` property from widget to the model. This way we get a random color when the model is created and we don't have to assign a random color each time `CountryList` widget is built. `Country.fromJson` is a named constructor used to create a model from parsed JSON data.
> While importing from a package, use `show` keyword to just import the members we need from the package instead of importing the whole package. In the line `import 'dart:math' show Random;`, we just imported Random() function from `dart:math` package.

## Dart async concepts
We are going to define the operation of fetching the data from API as a asynchronous operation. Asynchronous operations let our program complete work while waiting for another operation to finish. 

**Future**
A future is an instance of the Future class. A future represents the result of an asynchronous operation, and can have two states: uncompleted or completed. When data type of a variable is `Future<T>`, it means variable will have type of T when `Future` is completed in the future.

**Async and Await**
The async and await keywords provide a declarative way to define asynchronous functions and use their results. Remember these two basic guidelines when using async and await:
* To define an async function, add async before the function body.
* The await keyword works only in async functions.

Example:
```dart
Future<String> createOrderMessage() async {
  var order = await fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is
    // more complex and slow.
    Future.delayed(
      Duration(seconds: 2),
      () => 'Large Latte',
    );

Future<void> main() async {
  print('Fetching user order...');
  print(await createOrderMessage());
}
```
Result:
```
Fetching user order...
Your order is: Large Latte
```

## Let's fetch some data
Code for fetching data and converting it into an usable format:
```dart
Future<List<Country>> fetchCountries() async {
    //Create an instance of Dio class
    final Dio dio = Dio();
    //Empty countries list
    List<Country> countries = [];
    try {
      //Perform a get request on the given url
      final response = await dio.get("https://api.first.org/data/v1/countries");

      if (response.statusCode == 200) {
        //If status code is 200
        //Extract data property containing countries detail from response json.
        final Map<String, dynamic> data = response.data["data"];
        //Loop through all the keys in the data map
        for (MapEntry entry in data.entries) {
          //Create Country model from each entry in the map and add to it countries list
          countries
              .add(Country.fromJson(countryCode: entry.key, data: entry.value));
        }
      }
      return countries;
    } on DioError catch (e) {
      //On fetch error raise an exception
      throw Exception('Failed to fetch country data');
    }
  }
```
Here we first create an instance of `Dio` client. This instance will be used to make http requests. Next we assign an empty country list. Next we make a `GET` request using the `Dio` client. We store the result of the operation in response varialbe. Since this operation returns a `Future`, we have await the result of the operation using `await` keyword. This response object will contain all the information related to the request like status code of the request, response data etc. Response json data can be accessed from `response.data`. `Dio` automatically decode json into a dart `Map<String,dyanmic>`. So we don't to manually decode this json data.
We check if the status code of the response is `200` or not. If the status code of the response is 200, we get the data property from `response.data`. We only need data property from `response.data`. Response data is a `Map`. So we loop through it using `for..in` loop and create a `Country` object from each entry of the map(using `Country.fromJson` constructor) and then add it to `countryList`.
We put data fetching code in a `try` block to make sure that if certain errors occur during request, we can catch those errors in the corresponding `catch` block. In this code we used `on DioError` because we are catching a specific error ie.`DioError`. It is the error reported by `Dio` client. If `Dio` client encounters status code of the request as 400, 403, 404, 500 etc., it will report `DioError`. So in the `catch` block, we can take action depending on the error. In the above code, on encountering error we are raising a custom exception. 
This function returns `Future<List<Country>>` meaning the result obtained by calling this function will be resolved to `List<Country>`. 

## FutureBuilder
It is a widget that builds itself based on the latest snapshot of interaction with a Future. FutureBuilder takes a future as a property and based on state of future, it builds itself. It also takes a `builder` function. `builder` function takes two argument ie. BuildContext and AsyncSnapshot.
Snapshot can have three connection state ie. waiting, active and done. Based on connection state we can display different widgets. For example, when the connection state is `waiting` we can display a circular progress indicator and when the connection state is done we can display the data within the UI we previously built.
We also need to check for errors encountered during resolution of future and display the correct error message to the user. Snapshot has a `hasError` property for this purpose. If `hasError` is true, we can display error from `snapshot.error.toString()`.
We are also going to display country region(continent) below the country name in a `Column`.
> You can learn more about `FutureBuilder` from this [page](https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html).

```dart
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'dart:math';
import '../models/Country.dart';

class CountryList extends StatefulWidget {
  @override
  _CountryListState createState() => _CountryListState();
}

class _CountryListState extends State<CountryList> {
  Future<List<Country>> _fetchedCountries;

  Future<List<Country>> fetchCountries() async {
    //Create an instance of Dio class
    final Dio dio = Dio();
    //Empty countries list
    List<Country> countries = [];
    try {
      //Perform a get request on the given url
      final response = await dio.get("https://api.first.org/data/v1/countries");

      if (response.statusCode == 200) {
        //If status code is 200
        //Extract data property containing countries detail from response json.
        final Map<String, dynamic> data = response.data["data"];
        //Loop through all the keys in the data map
        for (MapEntry entry in data.entries) {
          //Create Country model from each entry in the map and add to it countries list
          countries
              .add(Country.fromJson(countryCode: entry.key, data: entry.value));
        }
      }
      return countries;
    } on DioError catch (e) {
      //On fetch error raise an exception
      throw Exception('Failed to fetch country data');
    }
  }

  @override
  void initState() {
    super.initState();
    setState(() {
      _fetchedCountries = fetchCountries();
    });
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _fetchedCountries,
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.hasError) {
            return Center(
              child: Text(
                snapshot.error.toString(),
                style: TextStyle(fontSize: 18),
              ),
            );
          }
          if (snapshot.hasData) {
            List<Country> countries = snapshot.data;
            return countryListView(countries: countries);
          }
        }
      },
    );
  }

  Widget countryListView({List<Country> countries}) {
    return ListView.builder(
      itemCount: countries.length,
      itemBuilder: (BuildContext context, int index) {
        Country country = countries[index];
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
          child: Container(
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.all(Radius.circular(10)),
              boxShadow: [
                BoxShadow(
                  color: Colors.grey.withOpacity(0.2),
                  spreadRadius: 2,
                  blurRadius: 3,
                  offset: Offset(0, 3), // changes position of shadow
                ),
              ],
            ),
            height: 70,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  CircleAvatar(
                    backgroundColor: Color(
                      (Random().nextDouble() * 0xFFFFFF).toInt(),
                    ).withOpacity(1.0),
                    child: Text(
                      country.code,
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                  Expanded(
                    child: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 8.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          FittedBox(
                            fit: BoxFit.contain,
                            child: Text(
                              country.name,
                              style: TextStyle(fontSize: 18),
                            ),
                          ),
                          Text(
                            country.region,
                            style: TextStyle(
                              fontSize: 14,
                              color: Color(0Xff3366ff),
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}
```
> `_fetchedCountries` is a state variable. It is of type `Future<List<Country>>` meaning it is a future which will resolve into `List<Country>`. This future will be used in the `FutureBuilder`.
`initState` method is a builtin method. It is called automatically when the widget is inserted into the widget tree. So if you want to take an operation when the widget is first build, you can do it in `initState`. Here we set `_fetchedCountries` to the future returned from `fetchCountries` function.
In `FutureBuilder`, we check for the connection state. If it is `waiting`, we display a `CircularProgressIndicator` widget. While if it is `done`, we check if snapshot encountered some error using `snapshot.hasError`. If `snapshot.hasError` is true, we display the error else display `snapshot.data` in a listview.
`FittedBox` widget scale the content so that the content fit within itself. It prevents widget from overflowing. You can learn more about `FittedBox` from this [page](https://api.flutter.dev/flutter/widgets/FittedBox-class.html).

![Screenshot 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/19hps2323uo4rowm24v3.gif)

And when the data is turned off:
![Screenshot 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m37mkbkj95sq90gl0z52.jpeg)

## Wrapping up
So we are done with this part of tutorial. In this tutorial we learned to make our app dynamic by fetching data from an API. In the next tutorial we will learn how to manage our app state properly.
You can always find the source code of this tutorial on github.
{% github tanweer919/Flutter-tutorial %}
> The source code for all of the app we are going to build in this series will live in different directory of this repo. For this tutorial source code is in **country_list**.

So that is it for me. See in you next tutorial. Thank you and Goodbye👋👋.