Consumo eficiente de API com Dio no Flutter

Tempo de leitura: 7 minutes

Para pessoas como eu, que se candidataram a milhares de empregos no Flutter, você pode testemunhar que “CONSUMIR APIS REST” é sempre um requisito nessas funções-chave. Hoje, criaremos um aplicativo simples de comércio eletrônico que exibe a lista de produtos de uma API usando o Dio.

Vamos começar

Vamos replicar esse design….

Para usar o Dio, digite em seu terminal

flutter pub add dio

Crie uma instância dele para referenciá-lo facilmente em qualquer lugar da tela do seu aplicativo

import 'package:dio/dio.dart';

final dio = Dio();

Lembre-se de que tínhamos esta url+endpoint para obter todos os produtos

https://fakestoreapi.com/products

Vamos verificar se funciona

Para fazer isso, crie uma função chamada “fetchProducts

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

Aqui, criamos uma função simples que busca todos os produtos desse endpoint, mas queremos verificar se ela realmente funciona antes de criar nosso aplicativo. Para isso, teríamos de executar essa função antes de criar toda a árvore de widgets do nosso aplicativo.

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

Portanto, você deve ter uma visualização completa como esta

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();
  }
}

Depois de fazer isso, você deverá obter dados JSON em seu 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
       }
   },

Os dados são mais do que isso. Eu usei isso para mostrar quais deveriam ser os resultados.

Metade do trabalho já foi feito.

Vamos criar a interface do usuário

Você notaria na imagem fornecida que há a imagem do produto, o nome do produto, a etiqueta de preço do produto e, em seguida, a classificação. Você não vai gostar de inseri-los um após o outro. Você gostaria?

Para fazer isso, vamos criar uma classe em outro arquivo dart com todos esses recursos

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

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

Você também notará que os produtos são projetados da mesma forma, mas apenas com textos ou parâmetros diferentes.

Crie outro arquivo chamado “demopage.dart” que lida com todas essas diferenças na interface do usuário e cole os seguintes códigos

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),
            ),
              
          ),
        ],
      ),
    );
  }
}

Em outras palavras, criei um widget sem estado que tem um contêiner. No contêiner, há um widget de coluna que resolve toda a lógica em relação à imagem, ao nome ou ao preço do produto.

Vamos voltar à nossa página TestScreen

Antes de chamar nossa API, vamos colocá-la em um dado de demonstração para ver se tudo funciona corretamente.

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]);
            },
          ),
        
        
        ],
      ),
    );

  
);
  }
}

Portanto, em termos simples, estamos dizendo que, a partir da classe Product, criamos uma visualização de grade que exibe a lista de produtos.

Funciona!

 

Agora, vamos trabalhar com nossa API.

Você deve se lembrar que imprimiu dados JSON no console. Seu trabalho como engenheiro do Flutter é garantir que os dados sejam exibidos na UI.

Como você faz isso?

É bem simples.

Lembre-se de que criamos manualmente uma classe Product que tem um nome String, um texto String e uma imagem String.

Além disso, para nossos dados JSON, você também pode criar a classe manualmente, mas para alguém como eu, que gosta de “faaji express(soft life)”, há uma maneira mais rápida de fazer isso.

Copie os dados JSON e cole-os em https://app.quicktype.io/. Ele criará um arquivo de classe para o seu aplicativo. Você terá o seguinte

// 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;
    }
}

Com isso feito, podemos integrá-lo ao nosso aplicativo Flutter.

Volte para sua TestScreenPage

Vamos fazer um pequeno ajuste na função fetch products que criamos anteriormente

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');
  }
}

Vou explicar esse código;

  • A função fetchProducts é definida como um Future que retorna void. Ela é marcada como assíncrona para permitir o uso de await dentro da função.
  • Dentro da função, um bloco try é usado para tratar qualquer erro potencial que possa ocorrer durante a execução.
  • A palavra-chave await é usada para fazer uma solicitação HTTP GET assíncrona usando o pacote Dio. A instância _dio (que foi inicializada anteriormente) é usada para fazer a solicitação.
  • O URL “https://fakestoreapi.com/products” é o ponto de extremidade do qual estamos obtendo os dados do produto.
  • A palavra-chave await indica que a execução da função será pausada até que uma resposta seja recebida da API. A resposta é atribuída à variável response.
  • A propriedade statusCode do objeto response é verificada para garantir que a solicitação foi bem-sucedida. Um código de status 200 indica uma solicitação bem-sucedida.
  • Se o código de status for 200, os dados da resposta serão obtidos usando a propriedade data de response. Nesse caso, espera-se que os dados da resposta sejam uma lista de objetos dinâmicos que representam os produtos.
  • A variável responseData é declarada como uma List<dynamic> e recebe o valor dos dados da resposta.
  • A função setState é chamada para atualizar o estado do widget.
  • A lista de products recebe o valor obtido pelo mapeamento de cada item na lista responseData para um objeto Products usando o método de fábrica Products.fromJson. O método toList é usado para converter o iterável mapeado em uma lista.
  • Se o código de status não for 200, indicando uma solicitação com falha, uma mensagem de erro será impressa no console.
  • O bloco catch é usado para capturar e tratar quaisquer erros que possam ocorrer durante a execução da função. A variável error contém a mensagem de erro.

Vamos juntar tudo isso

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]);
        },
      ),
    );
  }
}

Com isso, você está pronto para começar. Você acabou de criar um aplicativo a partir de uma API de comércio eletrônico usando o Dio.

Espero que você tenha se divertido bastante.