Flutter: Rede e API REST
Um app sem rede é uma ilha — requisições de API são a ponte para o continente.
📋 Pré-requisitos: Você precisa dominar o seguinte primeiro
- Aula 10: Lista e Rolagem
1. O Que Você Vai Aprender
- Pacote
httprequisições básicas GET/POST/PUT/DELETE Dioem profundidade: interceptores, CancelToken, upload FormData, retry com timeout- Serialização JSON:
json_serializable+build_runnergeração de código vs parsing manual - Estratégia de tratamento de erros: exceções de rede / 401 autenticação / 500 erros de servidor interceptação unificada
- Camada Repository com Dio do ShopApp, integrando com o endpoint
/api/v1/products
2. Uma História Real de Falhas em Requisições de Rede
(1) O Problema: Erros de Rede Derrubam o App
No dia de lançamento do ShopApp, usuários com conexões instáveis obtiveram crashes de tela branca — sem try-catch, sem timeout, sem retry. Pior ainda, quando o Token expirava, todas as requisições retornavam 401 e o app não tinha mecanismo de auto-refresh, forçando os usuários a fazer login novamente. 2 mil avaliações negativas em um dia.
(2) A Solução com Interceptor do Dio
O mecanismo de interceptores do Dio pode anexar tokens uniformemente antes das requisições saírem, tratar automaticamente respostas 401 renovando tokens, e tentar novamente em timeouts de rede.
import 'package:dio/dio.dart';
// ⚙️ Install dependency: flutter pub add dio
// Assume token and refreshToken are defined elsewhere
// String token = '...';
// Future<String> refreshToken() async { ... }
// Auth interceptor: auto-attach token + auto-refresh on 401
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
token = await refreshToken();
return handler.resolve(await dio.fetch(error.requestOptions));
}
handler.next(error);
},
));
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) Benefício: Zero Crashes Relacionados à Rede
Após usar interceptores Dio, Bob obtém auto-refresh de token em 401, retry 3x em timeout, e exibição unificada de erros. A taxa de crash relacionada à rede cai de 5% para 0.01%.
3. Requisições HTTP Básicas
(1) Operações CRUD do Pacote http
| Método | Verbo HTTP | Propósito |
|---|---|---|
http.get |
GET | Recuperar recursos |
http.post |
POST | Criar recursos |
http.put |
PUT | Atualizar recursos |
http.delete |
DELETE | Excluir recursos |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Requisições básicas com pacote http
import 'package:http/http.dart' as http;
import 'dart:convert';
// ⚙️ Install dependency: flutter pub add http
// Custom class definition sources:
// - Product: see Lesson 5 json_serializable model in this lesson
// - ApiException: see Lesson 6 unified error handling in this lesson
// Assume token is defined elsewhere
// String token = '...';
class ProductApi {
static const String baseUrl = 'https://api.shopapp.com/v1';
Future<List<Product>> getProducts({int page = 1}) async {
final response = await http.get(
Uri.parse('$baseUrl/products?page=$page'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return (data['items'] as List).map((j) => Product.fromJson(j)).toList();
}
throw ApiException(response.statusCode, response.body);
}
Future<Product> createProduct(Product product) async {
final response = await http.post(
Uri.parse('$baseUrl/products'),
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $token'},
body: jsonEncode(product.toJson()),
);
if (response.statusCode == 201) {
return Product.fromJson(jsonDecode(response.body));
}
throw ApiException(response.statusCode, response.body);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
4. Dio em Profundidade
(1) Comparação http vs Dio
| Recurso | http | Dio |
|---|---|---|
| CRUD básico | ✅ | ✅ |
| Interceptores | ❌ | ✅ |
| CancelToken | ❌ | ✅ |
| Upload FormData | Manual | ✅ |
| Retry com timeout | Manual | ✅ |
| Log de requisições | Manual | ✅ (dio_logger) |
| Configuração global | ❌ | ✅ BaseOptions |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Configuração básica do Dio
import 'package:dio/dio.dart';
// ⚙️ Install dependency: flutter pub add dio
// Custom class definition sources:
// - Product: see Lesson 5 json_serializable model in this lesson
// - Order/Cart: see Lesson 14 Phase 2 practice
final dio = Dio(BaseOptions(
baseUrl: 'https://api.shopapp.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
// GET request
Future<List<Product>> getProducts({int page = 1}) async {
final response = await dio.get('/products', queryParameters: {'page': page});
return (response.data['items'] as List).map((j) => Product.fromJson(j)).toList();
}
// POST request
Future<Order> createOrder(Cart cart) async {
final response = await dio.post('/orders', data: cart.toJson());
return Order.fromJson(response.data);
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Interceptor de autenticação + Renovação de Token
import 'package:dio/dio.dart';
// Custom class definition sources:
// - SecureStorage: see Lesson 13 Flutter Secure Storage
// - AuthService: see Lesson 14 Auth Notifier
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = SecureStorage.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
try {
final newToken = await _refreshToken();
SecureStorage.saveToken(newToken);
// Retry original request with new token
err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
final response = await dio.fetch(err.requestOptions);
return handler.resolve(response);
} catch (_) {
// Refresh failed, redirect to login
AuthService.logout();
}
}
handler.next(err);
}
Future<String> _refreshToken() async {
final refreshToken = SecureStorage.getRefreshToken();
final response = await Dio().post(
'https://api.shopapp.com/v1/auth/refresh',
data: {'refresh_token': refreshToken},
);
return response.data['access_token'];
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Cancelamento de requisição com CancelToken
import 'package:dio/dio.dart';
// ⚙️ Install dependency: flutter pub add dio
CancelToken? _cancelToken;
void searchProducts(String query) {
_cancelToken?.cancel('New search request');
_cancelToken = CancelToken();
dio.get('/products/search', queryParameters: {'q': query},
cancelToken: _cancelToken);
}
// Cancel on dispose
@override
void dispose() {
_cancelToken?.cancel();
super.dispose();
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
5. Serialização JSON
(1) Comparação de Métodos de Serialização
| Método | Prós | Contras | Caso de Uso |
|---|---|---|---|
| fromJson/toJson manual | Sem dependências, controle total | Repetitivo, propenso a erros | Poucos modelos simples |
| json_serializable | Segurança de tipos, auto-gerado | Requer build_runner | Muitos modelos |
| freezed | Imutável + copyWith + union | Mais dependências | Modelos de domínio complexos |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Modelo com json_serializable
import 'package:json_annotation/json_annotation.dart';
// ⚙️ Install dependency: flutter pub add json_annotation
// ⚙️ Dev dependency: flutter pub add --dev json_serializable build_runner
part 'product.g.dart';
@JsonSerializable()
class Product {
final int id;
final String name;
@JsonKey(name: 'unit_price')
final double price;
@JsonKey(name: 'image_url')
final String? imageUrl;
final double rating;
const Product({
required this.id,
required this.name,
required this.price,
this.imageUrl,
this.rating = 0.0,
});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
// Run: dart run build_runner build --delete-conflicting-outputs
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
6. Tratamento de Erros Unificado
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: ApiException + interceptor de erro
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
// ⚙️ Install dependency: flutter pub add dio
class ApiException implements Exception {
final int? statusCode;
final String message;
final String? errorCode;
ApiException(this.statusCode, this.message, {this.errorCode});
factory ApiException.fromDioError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return ApiException(null, 'Connection timeout. Please try again.');
case DioExceptionType.connectionError:
return ApiException(null, 'No internet connection.');
case DioExceptionType.badResponse:
final status = error.response?.statusCode;
final msg = error.response?.data['message'] ?? 'Server error';
if (status == 401) return ApiException(401, 'Session expired. Please login again.');
if (status == 403) return ApiException(403, 'Access denied.');
if (status == 404) return ApiException(404, 'Resource not found.');
if (status == 422) return ApiException(422, msg, errorCode: error.response?.data['code']);
if (status != null && status >= 500) return ApiException(status, 'Server error. Please try later.');
return ApiException(status, msg);
default:
return ApiException(null, 'Unexpected error.');
}
}
}
// Usage in UI
try {
final products = await productRepo.getProducts();
} on ApiException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message)),
);
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
7. Exemplo Completo: Repository Dio do ShopApp
import 'package:dio/dio.dart';
// ⚙️ Install dependency: flutter pub add dio
// Custom class definition sources:
// - Product: see Lesson 5 json_serializable model in this lesson
// - CartItem/Address/Order: see Lesson 14 Phase 2 practice
// - AuthInterceptor: see Lesson 4 Auth interceptor in this lesson
class ProductRepository {
final Dio _dio;
ProductRepository(this._dio);
Future<List<Product>> getProducts({
int page = 1,
int limit = 20,
String? category,
String? search,
}) async {
final params = {'page': page, 'limit': limit};
if (category != null) params['category'] = category;
if (search != null) params['q'] = search;
final response = await _dio.get('/products', queryParameters: params);
return (response.data['items'] as List)
.map((j) => Product.fromJson(j))
.toList();
}
Future<Product> getProductById(int id) async {
final response = await _dio.get('/products/$id');
return Product.fromJson(response.data);
}
Future<Order> createOrder(List<CartItem> items, Address address) async {
final response = await _dio.post('/orders', data: {
'items': items.map((i) => {'product_id': i.product.id, 'quantity': i.quantity}).toList(),
'shipping_address': address.toJson(),
});
return Order.fromJson(response.data);
}
Future<String> uploadProductImage(String filePath) async {
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(filePath),
});
final response = await _dio.post('/upload', data: formData);
return response.data['url'];
}
}
// Dio singleton setup
final dio = Dio(BaseOptions(
baseUrl: 'https://api.shopapp.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
))
..interceptors.add(AuthInterceptor())
..interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
final productRepo = ProductRepository(dio);
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
❓ Perguntas Frequentes
P: Como escolher entre Dio e o pacote http? R: O pacote http é suficiente para projetos pequenos; escolha Dio quando precisar de interceptores, cancelamento de requisições, retry, upload FormData, etc.
P: build_runner é lento toda vez? R: Durante o desenvolvimento, use
dart run build_runner watch --delete-conflicting-outputspara monitorar continuamente as mudanças de arquivo e gerar automaticamente.
P: Múltiplas requisições recebem 401 simultaneamente durante a renovação do token? R: Adicione um mecanismo de bloqueio: o primeiro 401 aciona a renovação, e as outras requisições 401 ficam na fila até a renovação ser concluída. Use um Completer para implementar isso.
P: Upload FormData de arquivos grandes causará OOM? R: MultipartFile.fromFile é carregado preguiçosamente e não lê o arquivo inteiro na memória de uma vez. Para arquivos grandes, considere upload em partes.
P: Como depurar requisições de rede no desenvolvimento? R: Adicione LogInterceptor para imprimir requisições/respostas, ou use Charles/Proxyman para captura de pacotes. Dio suporta configuração de proxy.
P: Verificação de certificado SSL falha? R: No desenvolvimento, use
HttpClientAdapterpara pular a verificação; em produção, os certificados devem ser verificados.
📖 Resumo
- O pacote http é adequado para cenários simples; Dio é para projetos em produção que precisam de interceptores, cancelamento e retry
- Interceptores tratam uniformemente anexação de token e auto-refresh em 401
- CancelToken cancela requisições obsoletas (ex.: debounce na caixa de busca)
- Geração de código json_serializable é segura quanto a tipos e reduz erros manuais
- ApiException envolve erros uniformemente; a camada de UI precisa capturar apenas um tipo de exceção
📝 Exercícios
- Básico (⭐): Use o pacote http para fazer uma requisição GET, buscar uma lista de produtos, e imprimir o JSON.
- Intermediário (⭐⭐): Envolva um Repository com Dio, adicione um interceptor de autenticação para auto-anexar Bearer Token, e auto-renovar em 401.
- Desafio (⭐⭐⭐): Implemente uma camada de rede completa: Dio + interceptor Auth + interceptor Log + CancelToken debounce na busca + ApiException tratamento unificado de erros + modelos json_serializable.