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 ✅
-
Observabilidade: Com o Dio, você vê exatamente o que entra e sai da rede com o
PrettyDioLogger. -
Flexibilidade: Se amanhã você trocar o Supabase por outro backend, sua camada de rede (Dio) já está isolada e pronta.
-
Tratamento de Erros: O Dio oferece tipos de erro específicos (
connectionTimeout,badResponse), facilitando a exibição de mensagens para o usuário.
Desvantagens ❌
-
Boilerplate: Exige mais classes iniciais do que apenas chamar o SDK do Supabase.
-
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:
-
Segurança Centralizada: Você não precisa adicionar o token manualmente em cada função
get()oupost(). O interceptor garante que, se o usuário estiver logado, o token estará lá. -
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. -
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
onErrordo 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?
- Economia de Bateria/Dados: O app nem tenta fazer a requisição se o rádio do celular souber que não há internet.
- UX Consistente: Todas as telas do seu app exibirão os mesmos erros para os mesmos problemas.
- 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:
-
Auth (Supabase) 🔐
-
Erro amigável (Dio) 💬
-
Verificação de Internet (Connectivity) 📡
-
Cache Offline (Hive) 📦