Consumindo REST APIs no Flutter
Requisições HTTP para REST APIs
Para chamar APIs, utilizamos o pacote http
. Este pacote já está instalado no projeto, mas caso queira instalar em outro projeto:
- Execute o comando
flutter pub add http
; - (Para projetos Android) No arquivo
AndroidManifest.xml
, adicione a permissão para acesso à Internet:<uses-permission android:name="android.permission.INTERNET" />
.
Para cada verbo, há um método associado (http.get()
, http.post()
, etc.). Esses métodos retornam um Future
contendo um http.Response
. Future
é uma classe utilizada para operações assíncronas, como é o caso de requisições HTTP, e representa um potencial valor ou erro que será retornado em algum momento no futuro. http.Response
é uma classe utilizada para armazenar o retorno de uma requisição.
Os códigos a seguir podem ser executados diretamente no DartPad (https://dartpad.dev).
import 'package:http/http.dart' as http;
Future<http.Response> fetchUsers() {
return http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
}
void users() async {
http.Response response = await fetchUsers();
String jsonBody = response.body;
print(jsonBody);
}
void main() {
users();
}
Com isso, já temos "em mãos" o retorno, porém em um formato "bruto" em uma String. No caso da API de nosso projeto, essa String contém um JSON.
Podemos decodificar o JSON utilizando um pacote nativo do Dart chamado dart:convert
. Este pacote expõe um método jsonDecode
, que utilizaremos a seguir:
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<http.Response> fetchUsers() {
return http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
}
Future<http.Response> fetchUser(int id) {
return http.get(Uri.parse('https://jsonplaceholder.typicode.com/users/$id'));
}
void users() async {
http.Response response = await fetchUsers();
List<dynamic> users = jsonDecode(response.body);
print(users);
}
void user() async {
http.Response response = await fetchUser(1);
Map<String, dynamic> user = jsonDecode(response.body);
print(user);
}
void main() {
users();
user();
}
Como o JSON contém essencialmente apenas dados no formato chave: valor
ou listas de chaves e valores, os retornos do método jsonDecode
podem ser Map<String, dynamic>
ou List<dynamic>
, dependendo do caso (ou simplesmente var
se estiver com preguiça).
Convertendo de JSON para modelos e vice-versa
Como vimos anteriormente, é fácil de obter dados de API. Porém, não há ferramentas nativas para serialização e desserialização automática de classes modelo. Essa operação deve ser feita manualmente, o que pode ser trabalhoso e propenso a erros.
O código a seguir possui duas classes, que idealmente devem ser inseridas em arquivos diferentes, porém no formato atual pode ser executada no DartPad.
import 'dart:convert';
// Item.dart
class Item {
final String description;
Item({required this.description});
@override
String toString() {
return 'Item (description = ${this.description})';
}
factory Item.fromJson(Map<String, dynamic> json) {
return Item(description: json['description']);
}
Map<String, dynamic> toJson(Item item) =>
<String, dynamic>{'description': item.description};
}
// Person.dart
// import 'Item.dart';
class Person {
final String name;
final DateTime dob;
final num height;
final num weight;
final List<Item> items;
@override
String toString() {
return 'Person (name = ${this.name}, dob = ${this.dob}, height = ${this.height}, weight = ${this.weight}, items = ${this.items})';
}
Person({
required this.name,
required this.dob,
required this.height,
required this.weight,
required this.items,
});
factory Person.fromJson(Map<String, dynamic> json) {
return Person(
name: json['name'],
dob: DateTime.parse(json['dob'] as String),
height: json['height'],
weight: json['weight'],
items: (json['items'] as List<dynamic>)
.map((e) => Item.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson(Person person) => <String, dynamic>{
'name': person.name,
'dob': person.dob,
'height': person.height,
'weight': person.weight,
};
}
void person() async {
Map<String, dynamic> person = jsonDecode('{"name": "Uncle Bob", "dob": "1980-02-28", "height": 1.8, "weight": 75.6, "items": [{"description": "Wooden sword"}, {"description": "Vibranium shield"}]}');
print(person);
Person model = Person.fromJson(person);
print(model);
}
void main() {
person();
}
Para facilitar o desenvolvimento, utilizaremos os pacotes json_annotation
, json_serializable
e build_tools
. Estes pacotes já estão instalados no projeto, mas, se quiser instalá-los, execute os seguintes comandos:
flutter pub add json_annotation
flutter pub add -d json_serializable build_tools
Agora, podemos criar as classes da seguinte forma:
// Item.dart
import 'package:json_annotation/json_annotation.dart';
part 'Item.g.dart';
@JsonSerializable()
class Item {
final String description;
Item({required this.description});
factory Item.fromJson(Map<String, dynamic> json) => _$ItemFromJson(json);
Map<String, dynamic> toJson() => _$ItemToJson(this);
}
// Person.dart
import 'package:json_annotation/json_annotation.dart';
part 'Person.g.dart';
@JsonSerializable()
class Person {
final String name;
final DateTime dob;
final num height;
final num weight;
final List<Item> items;
Person({
required this.name,
required this.dob,
required this.height,
required this.weight,
required this.items,
});
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
}
A linha que contém part '[Classe].g.dart
referencia um arquivo que ainda não existe e será gerado automaticamente. Para isso, é necessário iniciar um processo, que ficará executando em background, monitorando o código. Execute o seguinte comando:
flutter pub run build_runner watch
Ao iniciar o processo, os arquivos *.g.dart
serão criados. Se alguma classe for modificada, esses arquivos serão atualizados automaticamente, desde que o processo esteja em execução. Por isso, é recomendável reservar uma janela de terminal para ele.
É possível que o processo encontre erros e feche. Nesse caso, o erro deve ser corrigido e o processo executado novamente.
Caso ocorra um erro iniciando com Conflicting outputs were detected and the build is unable to prompt for permission to remove them. These outputs must be removed manually or the build can be run with ``--delete-conflicting-outputs``.
, delete todos os arquivos *.g.dart e execute o processo novamente.
Ainda há bastante código para escrever ao criar uma classe. Para facilitar, existe uma extensão do VSCode chamada Flutter Helpers (https://marketplace.visualstudio.com/items?itemName=aksharpatel47.vscode-flutter-helper). Esta extensão possui 2 snippets (jsf
e jsc
) e 2 comandos para executar o processo que gera os arquivos *.g.dart
(um para executar uma vez e outro para iniciar o monitoramento).
Criando um modelo |
Alterando um modelo |
Referências
- flutter.dev - Fetch data from the internet: https://flutter.dev/docs/cookbook/networking/fetch-data
- flutter.dev - JSON and serialization: https://flutter.dev/docs/development/data-and-backend/json
- pub.dev - Response class: https://pub.dev/documentation/http/latest/http/Response-class.html
- api.flutter.dev - Future<T> class: https://api.flutter.dev/flutter/dart-async/Future-class.html