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.
Flutter Series - Create your first app in Flutter
Tanweer Anwar ・ May 30 '21
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),
),
),
),
],
),
),
),
),
);
}
}
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),
),
),
),
],
),
),
),
),
);
}
}
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(),
),
);
}
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),
),
),
),
],
),
),
),
),
);
}
}
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(),
);
}
}
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),
),
),
),
],
),
),
),
),
);
}
}
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),
),
);
}
}
API for country data
We are going to fetch data from an API using HTTP requests. The url of the API:
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"
},
...
}
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);
}
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
showkeyword to just import the members we need from the package instead of importing the whole package. In the lineimport 'dart:math' show Random;, we just imported Random() function fromdart:mathpackage.
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());
}
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:
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
FutureBuilderfrom 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),
),
)
],
),
),
),
],
),
),
),
);
},
);
}
}
_fetchedCountriesis a state variable. It is of typeFuture<List<Country>>meaning it is a future which will resolve intoList<Country>. This future will be used in theFutureBuilder.
initStatemethod 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 ininitState. Here we set_fetchedCountriesto the future returned fromfetchCountriesfunction.
InFutureBuilder, we check for the connection state. If it iswaiting, we display aCircularProgressIndicatorwidget. While if it isdone, we check if snapshot encountered some error usingsnapshot.hasError. Ifsnapshot.hasErroris true, we display the error else displaysnapshot.datain a listview.
FittedBoxwidget scale the content so that the content fit within itself. It prevents widget from overflowing. You can learn more aboutFittedBoxfrom this page.
And when the data is turned off:

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


Top comments (0)