DEV Community

loading...

Flutter Series - Fetch data from the internet

tanweer919 profile image Tanweer Anwar ・13 min read

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.


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:

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),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Screenshot 1

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.

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),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

import 'package:flutter/material.dart';
import 'screens/HomeScreen.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to import this widget in HomeScreen.dart.

screens/HomeScreen.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(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

{
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"
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

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. This packages allows quickly building an app without having to develop everything from scratch.
For this project, we are going to install 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:

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

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());
}
Enter fullscreen mode Exit fullscreen mode

Result:

Fetching user order...
Your order is: Large Latte
Enter fullscreen mode Exit fullscreen mode

Let's fetch some data

Code for fetching data and converting it into an usable format:

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');
    }
  }
Enter fullscreen mode Exit fullscreen mode

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.

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),
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

_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.

Screenshot 2

And when the data is turned off:
Screenshot 3

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.

Flutter tutorial

This repo contains all the apps built in Flutter tutorial series.

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👋👋.

Discussion (0)

Forem Open with the Forem app