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 umMap
com os atributos necessários. - o método
toJson()
é responsável por transformar uma instância da classe em umMap
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 tipoServiceError
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.