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

1. O Que Você Vai Aprender


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.

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

TEXT
> 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

DART
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);
  }
}
TEXT
> 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

TEXT
> 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

DART
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);
}
TEXT
> 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

TEXT
> 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

DART
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'];
  }
}
TEXT
> 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

TEXT
> 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

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

TEXT
> 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

DART
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
TEXT
> 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

TEXT
> 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

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

DART
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);
TEXT
> 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-outputs para 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 HttpClientAdapter para pular a verificação; em produção, os certificados devem ser verificados.


📖 Resumo


📝 Exercícios

  1. Básico (⭐): Use o pacote http para fazer uma requisição GET, buscar uma lista de produtos, e imprimir o JSON.
  2. Intermediário (⭐⭐): Envolva um Repository com Dio, adicione um interceptor de autenticação para auto-anexar Bearer Token, e auto-renovar em 401.
  3. Desafio (⭐⭐⭐): Implemente uma camada de rede completa: Dio + interceptor Auth + interceptor Log + CancelToken debounce na busca + ApiException tratamento unificado de erros + modelos json_serializable.

← Aula Anterior | Próxima Aula →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%