Flutter tutorial
This repo contains all the apps built in Flutter tutorial series.
Posted on June 5, 2021
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.
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),
),
),
),
],
),
),
),
),
);
}
}
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(),
),
);
}
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(),
);
}
}
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),
),
);
}
}
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.
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.
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
show
keyword 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:math
package.
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:
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
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>
.
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),
),
)
],
),
),
),
],
),
),
),
);
},
);
}
}
_fetchedCountries
is 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
.
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 ininitState
. Here we set_fetchedCountries
to the future returned fromfetchCountries
function.
InFutureBuilder
, we check for the connection state. If it iswaiting
, we display aCircularProgressIndicator
widget. While if it isdone
, we check if snapshot encountered some error usingsnapshot.hasError
. Ifsnapshot.hasError
is true, we display the error else displaysnapshot.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 aboutFittedBox
from this page.
And when the data is turned off:
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.
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👋👋.
Posted on June 5, 2021
Sign up to receive the latest update from our blog.
July 31, 2024