Efficient API Consumption with Dio in Flutter III

Jesutoni Aderibigbe
8 min readJul 8, 2023

--

For folks like me who have applied to thousands of jobs on Flutter, you can testify that “CONSUMING REST APIS” is always a requirement in those key roles. Today, we will be building just a simple e-commerce app that displays the list of products from an API using Dio.

Let’s start

We will replicate this design….

I got this design from https://www.behance.net/gallery/87104663/E-commerce-App

To use Dio, type in your terminal

flutter pub add dio

Create an instance of it to easily reference it anywhere on the screen of your app

import 'package:dio/dio.dart';

final dio = Dio();

Recall that we had this url+endpoint to get all products

https://fakestoreapi.com/products

Let’s check if it works

To do this, create a function called “fetchProducts”

void fetchProducts()async{
final response = await dio.get("https://fakestoreapi.com/products")
print(response)
}

Here, we created a simple function that fetches all products from that endpoint but we want to check if it actually works before building our app. To do this, we would have to run this function before building the entire widget tree of our app.

 @override
void initState() {
super.initState();
fetchProducts();
}

So you should have a complete view like this

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class TestScreen extends StatefulWidget {
const TestScreen({super.key});

@override
State<TestScreen> createState() => _TestScreenState();
}

class _TestScreenState extends State<TestScreen> {
final dio = Dio();

void fetchProducts() async {
var result = dio.get('https://fakestoreapi.com/products');
}

initState() {
fetchProducts();
super.initState();
}

@override
Widget build(BuildContext context) {
return const Scaffold();
}
}

Having done this, you should get JSON data in your console

 {
"id": 1,
"title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
"price": 109.95,
"description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
"rating": {
"rate": 3.9,
"count": 120
}
},
{
"id": 2,
"title": "Mens Casual Premium Slim Fit T-Shirts ",
"price": 22.3,
"description": "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg",
"rating": {
"rate": 4.1,
"count": 259
}
},
{
"id": 3,
"title": "Mens Cotton Jacket",
"price": 55.99,
"description": "great outerwear jackets for Spring/Autumn/Winter, suitable for many occasions, such as working, hiking, camping, mountain/rock climbing, cycling, traveling or other outdoors. Good gift choice for you or your family member. A warm hearted love to Father, husband or son in this thanksgiving or Christmas Day.",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg",
"rating": {
"rate": 4.7,
"count": 500
}
},
{
"id": 4,
"title": "Mens Casual Slim Fit",
"price": 15.99,
"description": "The color could be slightly different between on the screen and in practice. / Please note that body builds vary by person, therefore, detailed size information should be reviewed below on the product description.",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/71YXzeOuslL._AC_UY879_.jpg",
"rating": {
"rate": 2.1,
"count": 430
}
},
{
"id": 5,
"title": "John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet",
"price": 695,
"description": "From our Legends Collection, the Naga was inspired by the mythical water dragon that protects the ocean's pearl. Wear facing inward to be bestowed with love and abundance, or outward for protection.",
"category": "jewelery",
"image": "https://fakestoreapi.com/img/71pWzhdJNwL._AC_UL640_QL65_ML3_.jpg",
"rating": {
"rate": 4.6,
"count": 400
}
},

The data is more than this. I took this to display what the results should be.

We have had half of the work done already.

Let’s build the UI

You would notice from the image provided that there is the image of the product, the name of the product, the price tag of the product, and then the rating. You won’t love to be inserting them one after the other. Would you?

To have this done, let’s create a class in another dart file with all these features

class Product {
final String name;
final String price;
final String image;

Product({required this.name, required this.price, required this.image});
}

You would also notice that the products are designed the same way but with just different texts or parameters.

Create another file named “demopage.dart” which handles all these differences in the UI and paste the following codes

class ProductItem extends StatelessWidget {
final Product product;

ProductItem({required this.product});

@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
product.image,
fit: BoxFit.cover,
height: 120,
width: double.infinity,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
product.name,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0, bottom: 8.0),
child: Text(
'\$${product.price}',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.only(left: 8.0, bottom: 8.0),
child: Text(
'BUY NOW!',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),

),
],
),
);
}
}

In other words, I created a stateless widget that has a container. In the container, there is a column widget that solves all the logic as regards the product image, name, or price.

Let’s get back to our TestScreen Page

Before calling our API, let’s put it in a demo data to see if all works correctly.

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class TestScreen extends StatefulWidget {
const TestScreen({super.key});

@override
State<TestScreen> createState() => _TestScreenState();
}

class _TestScreenState extends State<TestScreen> {
final dio = Dio();

void fetchProducts() async {
var result = dio.get('https://fakestoreapi.com/products');
}

initState() {
fetchProducts();
super.initState();
}


final List<Product> products = [
Product(
name: 'Product 1',
price: '9.99',
image: 'assets/product1.png',
),
Product(
name: 'Product 2',
price: '19.99',
image: 'assets/product2.png',
),
// Add more products as needed
];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('E-commerce App'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Category 1',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: products.length,
itemBuilder: (context, index) {
return ProductItem(product: products[index]);
},
),


],
),
);


);
}
}

So we are in simple terms saying, that from the Class Product build a grid view that displays the list of products.

It works!

Now, let’s work with our API.

You would recall that you printed out JSON data in your console. Your job as a flutter engineer is to ensure the data is displayed in the UI.

How do you do that?

It is pretty simple.

Recall that we had manually created a class Product that has a String name, String text, and String image.

Also, For our JSON data, you can create the class manually too but for someone like me who likes “faaji express(soft life)”, there is a faster way that you can have it done.

Copy the JSON data and paste it on https://app.quicktype.io/. It will create a class file for you for your app. You will have this

// To parse this JSON data, do
//
// final products = productsFromJson(jsonString);

import 'dart:convert';

List<Products> productsFromJson(String str) => List<Products>.from(json.decode(str).map((x) => Products.fromJson(x)));

String productsToJson(List<Products> data) => json.encode(List<dynamic>.from(data.map((x) => x.toJson())));

class Products {
int id;
String title;
double price;
String description;
Category category;
String image;
Rating rating;

Products({
required this.id,
required this.title,
required this.price,
required this.description,
required this.category,
required this.image,
required this.rating,
});

factory Products.fromJson(Map<String, dynamic> json) => Products(
id: json["id"],
title: json["title"],
price: json["price"]?.toDouble(),
description: json["description"],
category: categoryValues.map[json["category"]]!,
image: json["image"],
rating: Rating.fromJson(json["rating"]),
);

Map<String, dynamic> toJson() => {
"id": id,
"title": title,
"price": price,
"description": description,
"category": categoryValues.reverse[category],
"image": image,
"rating": rating.toJson(),
};
}

enum Category { MEN_S_CLOTHING, JEWELERY, ELECTRONICS, WOMEN_S_CLOTHING }

final categoryValues = EnumValues({
"electronics": Category.ELECTRONICS,
"jewelery": Category.JEWELERY,
"men's clothing": Category.MEN_S_CLOTHING,
"women's clothing": Category.WOMEN_S_CLOTHING
});

class Rating {
double rate;
int count;

Rating({
required this.rate,
required this.count,
});

factory Rating.fromJson(Map<String, dynamic> json) => Rating(
rate: json["rate"]?.toDouble(),
count: json["count"],
);

Map<String, dynamic> toJson() => {
"rate": rate,
"count": count,
};
}

class EnumValues<T> {
Map<String, T> map;
late Map<T, String> reverseMap;

EnumValues(this.map);

Map<T, String> get reverse {
reverseMap = map.map((k, v) => MapEntry(v, k));
return reverseMap;
}
}

With this done, we can integrate this into our Flutter application.

Go back to your TestScreenPage

Let’s make a little adjustment to the fetch products function that we created earlier

  Future<void> fetchProducts() async {
try {
final response = await dio.get('https://fakestoreapi.com/products');

if (response.statusCode == 200) {
final List<dynamic> responseData = response.data;
setState(() {
products = responseData.map((item) => Products.fromJson(item)).toList();
});
} else {
print('Request failed with status code: ${response.statusCode}');
}
} catch (error) {
print('Error: $error');
}
}




Let me explain this code;

  • The fetchProducts function is defined as a Future that returns void. It is marked as async to enable the use of await inside the function.
  • Inside the function, a try block is used to handle any potential errors that might occur during the execution.
  • The await keyword is used to make an asynchronous HTTP GET request using the Dio package. The _dio instance (which was previously initialized) is used to make the request.
  • The URL 'https://fakestoreapi.com/products' is the endpoint from which we are fetching the product data.
  • The await keyword indicates that the execution of the function will pause until a response is received from the API. The response is assigned to the response variable.
  • The statusCode property of the response object is checked to ensure that the request was successful. A status code of 200 indicates a successful request.
  • If the status code is 200, the response data is obtained using the data property of the response. In this case, the response data is expected to be a list of dynamic objects representing the products.
  • The responseData variable is declared as a List<dynamic> and assigned the value of the response data.
  • The setState function is called to update the state of the widget.
  • The products list is assigned the value obtained by mapping each item in the responseData list to a Products object using the Products.fromJson factory method. The toList method is used to convert the mapped iterable into a list.
  • If the status code is not 200, indicating a failed request, an error message is printed to the console.
  • The catch block is used to catch and handle any errors that might occur during the execution of the function. The error variable contains the error message.

Let’s put all of this together

class TestScreen extends StatefulWidget {
@override
_TestScreenState createState() => _TestScreenState();
}

class _TestScreenState extends State<TestScreen> {
final Dio _dio = Dio();
List<Products> products = [];

@override
void initState() {
super.initState();
fetchProducts();
}

Future<void> fetchProducts() async {
try {
final response = await _dio.get('https://fakestoreapi.com/products');

if (response.statusCode == 200) {
final List<dynamic> responseData = response.data;
setState(() {
products = responseData.map((item) => Products.fromJson(item)).toList();
});
} else {
print('Request failed with status code: ${response.statusCode}');
}
} catch (error) {
print('Error: $error');
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('E-commerce App'),
),
body: GridView.builder(
padding: EdgeInsets.all(16.0),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: products.length,
itemBuilder: (context, index) {
return ProductItem(product: products[index]);
},
),
);
}
}

With this, you are good to go. You just built an app from an e-commerce API using Dio.

I hope you had a great time.

--

--