Flutter: Requisições HTTP

Carlos Costa • 13/11/2024 Ao longo desse post vamos entender como fazer requisições HTTP no Flutter, veremos todos as etapas necessárias para consumir dados de uma API e exibir os dados na nossa UI.

Requisitando os dados a partir de uma API


Para começar faremos uma requisição básica usando o pacote http. Também vamos utlizar a API do JSONPlaceholder, que retorna dados fictícios. Os dados serão requisitados pela rota /users.

import 'dart:io';
import 'package:http/http.dart' as http;

sealed class Service {

  // client HTTP
  HttpClient client = HttpClient();

  static Future<http.Response> getUsers() async {

    // URL da API
    var url = Uri.parse("https://jsonplaceholder.typicode.com/users");

    var response = await http.get(url);

    return response;
  }
}

Aqui temos um método que faz uma requisição para a rota /users.

Quando printamos o body e o tipo da resposta, temos o seguinte resultado:

FilledButton(
  onPressed: () async {
    var response = await Service.getUsers();

    debugPrint(response.body.runtimeType.toString());
    debugPrint(response.body);
  },
  child: const Text('Request'),
)
👇
String
[
  {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
      "geo": {
        "lat": "-37.3159",
        "lng": "81.1496"
      }
    }
    "company": {
      "name": "Romaguera-Crona",
      "catchPhrase": "Multi-layered client-server neural-net",
      "bs": "harness real-time e-markets"
    }
  },
],

Recebemos uma respota do tipo String. O que vamos precisar para consumir esses dados na nossa UI é de uma lista de objetos do tipo User. O os próximos passos serão deserializer e adaptar os dados recebidos.

Serialização e Deserialização


Serialização: é o processo de transformar um objeto em uma string

Deserialização: é o processo de transformar uma string em um objeto

Ok, agora faremos mais algumas mudanças no nosso método getUsers().

sealed class Service {
  HttpClient client = HttpClient();

  static Future<(http.Response, dynamic)> getUsers() async {
    var url = Uri.parse("https://jsonplaceholder.typicode.com/users");

    var response = await http.get(url);

    //deserializing reponse body
    var users = jsonDecode(response.body);

    return (response, users);
  }
}

Aqui, além de retornar a resposta, também retornamos o body da resposta deserializado. Para deserializar o body, utilizamos a função jsonDecode(), que transforma a String em um objeto do tipo dynamic.

Agora vamos imprimir o runtimeType do body deserializado.

FilledButton(
  onPressed: () async {
    var (response, users) = await Service.getUsers();

    debugPrint('Response type: ${response.runtimeType}');
    debugPrint('Users type: ${users.runtimeType}');

    users.forEach((user) {
      debugPrint('User type: ${user.runtimeType}');
    });
  },
  child: const Text('Request'),
)
👇
Response type: Response
Users type: List<dynamic>
User type: _Map<String, dynamic>

Agora temos uma lista de objetos do tipo _Map<String, dynamic>. O passo final será transformar esses objetos em objetos do tipo User.

Adaptação dos dados


Para transformar os objetos do tipo _Map<String, dynamic> em objetos do tipo User vamos precisar fazer o mapeamento das classes referentes aos dados que recebemos na requisição.

Observando o seguinte JSON:

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  }
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

Podemos extrair as seguintes classes: User, Company, Address e Geo.

Criando a classe User:

import 'package:json_annotation/json_annotation.dart';
import '/modules/http_request/models/address/address.dart';
import '/modules/http_request/models/company/company.dart';

part 'user.g.dart';

@JsonSerializable(explicitToJson: true)
class User {
  final String name;
  final String username;
  final String email;
  final String phone;
  final String website;
  final Address address;
  final Company company;

  User({
    required this.name,
    required this.username,
    required this.email,
    required this.phone,
    required this.website,
    required this.address,
    required this.company,
  });

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Nessa classe temos a seguinte situação:

  • A classe User é anotada com @JsonSerializable(explicitToJson: true). Essa anotação é necessária para que o gerador de código do JSON possa gerar o código necessário para serializar e deserializar a classe.
  • o método fromJson() é responsável por criar uma instância da classe a partir de um Map com os atributos necessários.
  • o método toJson() é responsável por transformar uma instância da classe em um Map com os atributos do objeto.
  • user.g.dart é o arquivo gerado pelo build runner. Esse arquivo contém o código necessário para serializar e deserializar a classe.

As outras classes serão criadas seguindo o mesmo padrão.

//address.dart

import 'package:json_annotation/json_annotation.dart';
import '/modules/http_request/models/geo/geo.dart';

part 'address.g.dart';

@JsonSerializable(explicitToJson: true)
class Address {
  final String street;
  final String suite;
  final String city;
  final String zipcode;
  final Geo geo;

  Address({
    required this.street,
    required this.suite,
    required this.city,
    required this.zipcode,
    required this.geo,
  });

  factory Address.fromJson(Map<String, dynamic> json) =>
      _$AddressFromJson(json);

  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

//company.dart

import 'package:json_annotation/json_annotation.dart';

part 'company.g.dart';

@JsonSerializable(explicitToJson: true)
class Company {
  final String name;
  final String catchPhrase;
  final String bs;

  Company({
    required this.name,
    required this.catchPhrase,
    required this.bs,
  });

  factory Company.fromJson(Map<String, dynamic> json) =>
      _$CompanyFromJson(json);

  Map<String, dynamic> toJson() => _$CompanyToJson(this);
}
//geo.dart

import 'package:json_annotation/json_annotation.dart';

part 'geo.g.dart';

@JsonSerializable(explicitToJson: true)
class Geo {
  final String lat;
  final String lng;

  Geo({
    required this.lat,
    required this.lng,
  });

  factory Geo.fromJson(Map<String, dynamic> json) => _$GeoFromJson(json);

  Map<String, dynamic> toJson() => _$GeoToJson(this);
}

Fazendo mais uma mudança no nosso método getUsers():

sealed class Service {
  HttpClient client = HttpClient();

  static Future<(http.Response, List<User>)> getUsers() async {
    var url = Uri.parse("https://jsonplaceholder.typicode.com/users");

    var response = await http.get(url);

    //verifica se a requisição foi bem sucedida
    if (response.statusCode != 200) {
      throw Exception("Error while fetching data");
    }

    //deserializa o body da resposta
    var body = jsonDecode(response.body);

    //lista de users
    var users = <User>[];

    for (var user in body) {
      //adiciona cada user na lista
      users.add(User.fromJson(user));
    }

    return (response, users);
  }
}

Adicionamos uma verificação para saber se a requisição foi bem sucedida, caso não tenha sido, lançamos uma exceção do tipo Exception com uma mensagem de erro. Também usamos o método fromJson() para transformar cada objeto do tipo _Map<String, dynamic> em um objeto do tipo User.

Consumindo os dados da requisição


Para consumir esses dados vamos criar um mixin que vai ser responsável por gerenciar o estado da nossa UI.

import 'package:flutter/material.dart';
import './service.dart';
import './models/user/user.dart';

mixin UserQuery<T extends StatefulWidget> on State<T> {
  bool isLoading = true;
  bool isSucess = false;
  bool isError = false;
  List<User> users = [];
  ServiceError? error;

  Future<void> getUsers() async {
    try {
      var (_, data) = await Service.getUsers();

      setState(() {
        isLoading = false;
        isSucess = true;
        users = data;
      });
    } catch (e) {
      setState(() {
        isLoading = false;
        isError = true;
        error = e as ServiceError;
      });
    }
  }
}

Aqui temos um mixin que gerencia o estado da nossa UI. Ele possui os seguintes atributos:

  • isLoading: indica se a requisição está em andamento.
  • isSucess: indica se a requisição foi bem sucedida.
  • isError: indica se a requisição retornou um erro.
  • users: lista de usuários.
  • error: objeto do tipo ServiceError que representa o erro retornado pela requisição.

Também temos uma chamada ao método getUsers() que é responsável por fazer a requisição e gerenciar o estado da UI.

Agora vamos consumir esses dados na nossa UI. 👇

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

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

  @override
  State<HttpRequestPage> createState() => _HttpRequestPageState();
}

class _HttpRequestPageState extends State<HttpRequestPage> with UserQuery {
  @override
  void initState() {
    super.initState();

    getUsers();
  }

  @override
  Widget build(BuildContext context) {
    // Se isLoading for true, exibe um loading
    if (isLoading) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }

    // Se isError for true, exibe um Text com a mensagem de erro
    if (isError) {
      return Scaffold(
        body: Center(child: Text(errorMessage!)),
      );
    }

    // Se isSucess for true, exibe a lista de usuários
    return Scaffold(
      appBar: AppBar(
        title: const Text('HTTP Request'),
      ),
      body: ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) {
          var user = users[index];

          return ListTile(
            title: Text(user.name),
            subtitle: Text(user.email),
          );
        },
      ),
    );
  }
}

Nesse exemplo, temos uma tela que exibe uma lista de usuários. Quando o widget é iniciado, o método initState() é chamado e faz a requisição. Enquanto a requisição está em andamento, é exibido um CircularProgressIndicator. Se a requisição for bem sucedida, a lista de usuários é exibida. Se a requisição retornar um erro, é exibido um Text com a mensagem de erro.

Referências


O código completo desse post está disponível no GitHub.