Supabase + Dio: Arquitetura de Elite no Flutter 🛡️

Muitos desenvolvedores usam apenas o SDK oficial do Supabase. Porém, quando você precisa escalar e integrar Edge Functions ou APIs externas (como Gateways de Pagamento), o SDK puro pode ser limitado. É aqui que o Dio entra como o motor de rede definitivo.

🚀 Por que usar os dois juntos?

  • Supabase SDK: Excelente para Realtime, Auth e tabelas (Postgres).

  • Dio: Essencial para interceptores (logs de rede, refresh token customizado) e controle refinado de requisições HTTP para as funções de servidor.


🏗️ Projeto Demonstrativo: “Fake Store Sênior”

Vamos simular um projeto que consome o banco do Supabase para usuários, mas usa o Dio para buscar produtos de uma API Fake (Platzi Fake Store API).

1. Dependências Necessárias (pubspec.yaml)

dependencies:
  supabase_flutter: ^2.0.0
  dio: ^5.4.0
  pretty_dio_logger: ^1.3.1 # Para logs incríveis no console

2. O Cliente de Rede Customizado (Network Manager)

Um desenvolvedor sênior não espalha instâncias do Dio pelo app. Criamos uma classe singleton com interceptores.

import 'package:dio/dio.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';

class ApiService {
  late Dio dio;

  ApiService() {
    dio = Dio(
      BaseOptions(
        baseUrl: 'https://api.escuelajs.co/api/v1', // Fake API
        connectTimeout: const Duration(seconds: 5),
        receiveTimeout: const Duration(seconds: 3),
      ),
    );

    // Adicionando Log para Debugging (O truque do post anterior!)
    dio.interceptors.add(PrettyDioLogger(
      requestHeader: true,
      requestBody: true,
      responseHeader: false,
    ));
    
    // Interceptor para injetar o Token do Supabase se necessário
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        // Exemplo: Pegar token da sessão atual do Supabase
        // final token = supabase.auth.currentSession?.accessToken;
        // options.headers['Authorization'] = 'Bearer $token';
        return handler.next(options);
      },
    ));
  }
}

💡 Exemplo Avançado: Repositório Híbrido

Aqui combinamos o Supabase (para persistência de perfil) e o Dio (para dados externos).

class ProductRepository {
  final ApiService _api = ApiService();
  final _supabase = Supabase.instance.client;

  // Busca da API Fake via DIO
  Future<List<dynamic>> fetchProducts() async {
    try {
      final response = await _api.dio.get('/products');
      return response.data;
    } on DioException catch (e) {
      throw 'Erro na API: ${e.response?.statusCode}';
    }
  }

  // Salva "Favoritos" no Banco Real do SUPABASE
  Future<void> toggleFavorite(int productId) async {
    final userId = _supabase.auth.currentUser!.id;
    
    await _supabase.from('favorites').upsert({
      'user_id': userId,
      'product_id': productId,
    });
  }
}

📊 Vantagens vs. Desvantagens

Vantagens ✅

  1. Observabilidade: Com o Dio, você vê exatamente o que entra e sai da rede com o PrettyDioLogger.

  2. Flexibilidade: Se amanhã você trocar o Supabase por outro backend, sua camada de rede (Dio) já está isolada e pronta.

  3. Tratamento de Erros: O Dio oferece tipos de erro específicos (connectionTimeout, badResponse), facilitando a exibição de mensagens para o usuário.

Desvantagens ❌

  1. Boilerplate: Exige mais classes iniciais do que apenas chamar o SDK do Supabase.

  2. Manutenção: Você precisa manter as versões do SDK e do Dio compatíveis (geralmente tranquilo).


🛠️ Desafio Prático para Você

Tente implementar um Interceptor no Dio que verifique se o usuário ainda está logado no Supabase antes de cada requisição. Se a sessão expirou, o Interceptor cancela a requisição e redireciona para o Login.

Aqui está a implementação de um AuthInterceptor profissional, que verifica a sessão do Supabase e injeta o token automaticamente.

1. O Interceptor de Autenticação

Este componente intercepta a requisição antes dela sair do dispositivo.

import 'package:dio/dio.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

class SupabaseAuthInterceptor extends Interceptor {
  final SupabaseClient _supabase = Supabase.instance.client;

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // 1. Recupera a sessão atual
    final session = _supabase.auth.currentSession;

    if (session != null) {
      // 2. Se a sessão existe, injeta o Access Token no Header
      options.headers['Authorization'] = 'Bearer ${session.accessToken}';
      
      // 3. Opcional: Injeta a API Key do Supabase (exigida em Edge Functions)
      options.headers['apikey'] = _supabase.supabaseKey;
    }

    // Segue com a requisição
    return handler.next(options);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // 4. Se receber um erro 401 (Não autorizado), o token pode ter expirado
    if (err.response?.statusCode == 401) {
      print('Sessão expirada ou inválida. Redirecionando para login...');
      // Aqui você poderia disparar um evento para deslogar o usuário no Flutter
    }
    return handler.next(err);
  }
}

2. Integrando ao seu ApiService

Agora, configuramos o Dio para usar esse interceptor junto com o logger de rede.

class ApiService {
  final Dio dio = Dio();

  ApiService() {
    dio.options.baseUrl = 'https://sua-api-ou-edge-function.supabase.co/functions/v1';
    dio.options.connectTimeout = const Duration(seconds: 10);

    dio.interceptors.addAll([
      SupabaseAuthInterceptor(), // Nosso interceptor de segurança
      LogInterceptor(responseBody: true), // Para ver o que está acontecendo
    ]);
  }
}

3. Por que isso é “Nível Sênior”?

Ao usar essa abordagem, você ganha três benefícios imediatos:

  1. Segurança Centralizada: Você não precisa adicionar o token manualmente em cada função get() ou post(). O interceptor garante que, se o usuário estiver logado, o token estará lá.

  2. Sincronização de Estado: O Supabase gerencia o refresh do token automaticamente em segundo plano. O interceptor apenas pega o valor mais atual do currentSession.

  3. Tratamento de Erros Global: Se o seu backend retornar um erro de autenticação, o lugar para tratar o “Logout Forçado” é dentro do método onError do interceptor, mantendo seus widgets de UI limpos.

💡 Dica Extra: Testando com API Fake

Se você estiver usando uma API externa que não conhece o Supabase (como a Platzi Fake Store que mencionamos), você pode criar uma lógica no interceptor para não enviar o token do Supabase para URLs externas, evitando vazamento de credenciais:

if (session != null && options.path.contains('supabase.co')) {
  options.headers['Authorization'] = 'Bearer ${session.accessToken}';
}

Tratar a falta de internet (e a instabilidade de rede) de forma global é o que separa um app “protótipo” de um app pronto para produção. Em vez de colocar um try/catch em cada botão, usamos o Interceptor do Dio para centralizar essa lógica.

Aqui está como implementar um Connectivity Interceptor:

1. Dependência Necessária

Para detectar o estado real da rede (Wi-Fi, Dados Móveis ou nada), usamos o pacote oficial da comunidade Flutter:

dependencies:
  connectivity_plus: ^6.0.0

2. O Interceptor de Conexão

Este interceptor verifica se há internet antes de tentar enviar a requisição e também traduz os erros de “Timeout” para mensagens amigáveis.

import 'package:dio/dio.dart';
import 'package:connectivity_plus/connectivity_plus.dart';

class NetworkErrorInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    // 1. Verifica o status da conexão antes de sair
    var connectivityResult = await (Connectivity().checkConnectivity());
    
    if (connectivityResult.contains(ConnectivityResult.none)) {
      // 2. Lança um erro customizado sem nem tentar a rede
      return handler.reject(
        DioException(
          requestOptions: options,
          error: 'Sem conexão com a internet. Verifique seu Wi-Fi ou dados móveis.',
          type: DioExceptionType.connectionError,
        ),
      );
    }
    return handler.next(options);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // 3. Traduz erros técnicos para mensagens que o usuário entende
    String message;
    
    switch (err.type) {
      final String msg;
      case DioExceptionType.connectionTimeout:
        message = "O servidor demorou muito para responder. Tente novamente.";
      case DioExceptionType.receiveTimeout:
        message = "Falha ao receber dados do servidor.";
      case DioExceptionType.connectionError:
        message = "Não foi possível conectar ao servidor.";
      default:
        message = "Ocorreu um erro inesperado na rede.";
    }

    // Criamos um novo erro com a mensagem amigável
    final customError = DioException(
      requestOptions: err.requestOptions,
      response: err.response,
      type: err.type,
      error: message,
    );

    return handler.next(customError);
  }
}

3. Como fica a Arquitetura Final

Ao empilhar esses interceptores, sua requisição passa por uma “linha de montagem” de segurança e validação:

4. Usando na Interface (UI)

Agora, seu código no StatefulWidget ou Controller fica limpo, pois a mensagem de erro já vem mastigada:

try {
  final produtos = await productRepo.fetchProducts();
} on DioException catch (e) {
  // O 'e.error' aqui já conterá a frase "Sem conexão com a internet..." 
  // vinda do nosso interceptor!
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(e.error.toString())),
  );
}

Por que essa abordagem é superior?

  1. Economia de Bateria/Dados: O app nem tenta fazer a requisição se o rádio do celular souber que não há internet.
  2. UX Consistente: Todas as telas do seu app exibirão os mesmos erros para os mesmos problemas.
  3. Fácil de Testar: Você pode simular erros de rede em um único lugar para testar como o app se comporta.

Fazer o cache de requisições transforma a experiência do usuário, pois permite que o app exiba dados instantaneamente, mesmo em “zonas mortas” de sinal (como elevadores ou túneis).

Para isso, utilizaremos o dio_cache_interceptor, que é o padrão da indústria para essa funcionalidade no Flutter.


1. Adicione as dependências

Além do Dio, precisamos do interceptor de cache e de uma estratégia de armazenamento (usaremos o Hive por ser extremamente rápido para persistência local).

dependencies:
  dio: ^5.4.0
  dio_cache_interceptor: ^3.5.0
  dio_cache_interceptor_hive_store: ^3.2.2 # Para salvar no disco

2. Configurando o Gerenciador de Cache

O segredo está em configurar uma estratégia de “Cache First” ou “Refresh Force”.

import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
import 'package:path_provider/path_provider.dart';

class CacheManager {
  static late CacheOptions options;

  static Future<void> init() async {
    // 1. Define onde o cache será salvo (Pasta temporária do app)
    final dir = await getTemporaryDirectory();

    options = CacheOptions(
      // Usa Hive para persistência rápida
      store: HiveCacheStore(dir.path),
      // Tenta pegar do cache primeiro; se falhar ou expirar, vai na rede
      policy: CachePolicy.refreshForceCache,
      // Se a rede falhar, usa o cache mesmo que ele tenha expirado
      hitCacheOnErrorExcept: [401, 403],
      // Tempo de vida do cache (ex: 7 dias)
      maxStale: const Duration(days: 7),
      // Chave de prioridade para identificar a requisição
      priority: CachePriority.normal,
    );
  }
}

3. Integrando ao seu ApiService

Agora, basta adicionar o interceptor à sua lista existente. Sua “linha de montagem” de rede está ficando poderosa:

class ApiService {
  final Dio dio = Dio();

  ApiService() {
    dio.interceptors.addAll([
      // 1. Cache (Deve vir antes para retornar dados rápido)
      DioCacheInterceptor(options: CacheManager.options),
      
      // 2. Auth (Injeta o Token do Supabase)
      SupabaseAuthInterceptor(),
      
      // 3. Network Check (Verifica se há internet)
      NetworkErrorInterceptor(),
      
      // 4. Logger (Para debug)
      LogInterceptor(responseBody: true),
    ]);
  }
}

4. Como forçar uma atualização?

Às vezes o usuário faz um “Pull to Refresh” e você precisa ignorar o cache. Com o Dio, basta passar uma opção extra na chamada:

final response = await dio.get(
  '/products',
  options: CacheManager.options.copyWith(policy: CachePolicy.refresh).toOptions(),
);

Por que isso é importante para o seu projeto?

  • Economia de API: Você evita fazer requisições repetitivas ao Supabase ou APIs pagas, economizando dinheiro.

  • Velocidade Percebida: O app abre as telas instantaneamente porque os dados já estão no disco.

  • Resiliência: Se o seu servidor (ou o Supabase) cair por alguns minutos, o usuário nem perceberá, pois o app continuará mostrando os últimos dados baixados.

Com isso, fechamos uma Arquitetura de Rede completa:

  1. Auth (Supabase) 🔐

  2. Erro amigável (Dio) 💬

  3. Verificação de Internet (Connectivity) 📡

  4. Cache Offline (Hive) 📦

Please follow and like us:
error0
fb-share-icon
Tweet 20
fb-share-icon20