Utilizando o Dio para comunicação com APIs
A comunicação com APIs é crucial em aplicativos modernos para integrar dados em tempo real e funcionalidades externas, proporcionando experiências dinâmicas e interativas, e no desenvolvimento de aplicações Flutter não é diferente.
Duas das abordagens mais populares para fazer requisições HTTP em Flutter são o pacote HTTP nativo e o pacote Dio.
Embora ambos possam ser usados para realizar operações básicas de rede, o Dio oferece uma série de vantagens que fazem com que ele seja a escolha preferida das pessoas desenvolvedoras.
Neste artigo, vamos explorar os benefícios do uso do Dio em projetos Flutter. Vamos analisar suas características, facilidades de uso e como ele pode simplificar e aprimorar o desenvolvimento de suas aplicações móveis.
Neste artigo, você aprenderá:
- O que é o Dio
- Configuração inicial e instalação do Dio
- Realizando as requisições GET e POST
- Tratamento de erros
- Interceptadores
- Timeout e cancelamento de requisições
- Uso do Dio com Providers
- Autenticação e autorização JWT
- Requisições Multipart/Form-Data com Dio
- Cache de Requisições
O que é o Dio?
Dio é uma biblioteca de cliente HTTP para Flutter que simplifica o processo de fazer requisições a APIs.
Com ele, podemos fazer requisições GET, POST, PUT, DELETE e muito mais. Suas diversas funcionalidades tornam o Dio a escolha preferida para projetos Flutter. Funcionalidades essas como:
- Interceptores: Manipulação centralizada de requisições, respostas e erros.
- FormData: Envio fácil de dados de formulários, incluindo uploads de arquivos.
- Cancelamento de requisições: Interrompe requisições em andamento.
- Transformadores de dados: Modificação flexível de dados de entrada e saída.
- Retry: Tentativa automática de novas requisições em caso de falhas.
- Gerenciamento de erros: Tratamento detalhado de erros.
- Configuração de tempo limite: Ajuste de tempos limites para requisições e conexões.
Comparado ao pacote HTTP comum nativo, o Dio tem mais facilidade no uso, enquanto que o Dio é intuitivo e simplificado, com métodos que facilitam a configuração, já o HTTP requer um pouco mais de configuração manual.
Como configurar o DIO
Imagine que você acabou de lançar um blog de tecnologia, cheio de publicações que deseja compartilhar com o mundo.
Você decidiu criar um aplicativo Flutter para tornar mais fácil a visualização de todos esses conteúdos, mas se pergunta como fazer isso de forma eficiente. A resposta para essa pergunta é o Dio.
O primeiro passo é instalar o Dio no projeto com o comando:
flutter pub add dio
Pronto, agora que você tem o Dio instalado em seu projeto, você pode conferir indo até o arquivo pubspec.yaml
:
Como fazer requisições simples
Executando um GET:
Com a dependência adicionada, podemos criar um exemplo de requisição GET para coletar os dados do nosso blog. Para demonstrar como isso funciona, vamos usar a API pública do JSONPlaceholder, que fornece uma lista de posts fictícios. Faremos a requisição através do método _dio.get
e usaremos o método await
para garantir que aguardemos a resposta da API antes de prosseguir com qualquer outra ação. Veja a seguir um exemplo:
import 'package:dio/dio.dart';
final Dio _dio = Dio();
Future<void> getAllPosts() async {
Response response = await _dio.get('https://jsonplaceholder.typicode.com/posts');
print(response.data.toString());
}
Neste exemplo, fizemos uma requisição GET para a API e imprimimos todos os posts no console. Simples, não é mesmo?
Executando um POST:
Após aprender como mostrar todos os posts já feitos em nosso blog de tecnologia, você pode querer adicionar um novo conteúdo à sua página e fazemos isso através do método _dio.post
, como mostro a seguir:
Future<void> createPost() async {
Response response = await _dio.post('https://jsonplaceholder.typicode.com/posts',
data: {
'title': 'Feedback',
'body': 'Adorei aprender Dio!!',
'userId': 1,
},
);
print(response.data.toString());
}
Neste exemplo, enviamos os dados do post no corpo da requisição usando o parâmetro `data`. Após a execução da requisição, imprimimos a resposta no console que mostra os dados enviados.Tratamento de erros
Já aprendemos as operações básicas do Dio, como buscar e criar posts. Quando algo dá errado, como a internet cair, um servidor não responder ou o(a) usuário(a) fazer uma requisição inválida, o Dio lida com essas exceções através da DioException
.
Essa exceção é lançada quando ocorre um problema durante a requisição, como falhas de conexão ou respostas de status diferentes de 200 do servidor, indicando que houve algum erro na requisição.
Para contornar essas exceções, podemos utilizar blocos try-catch
ao redor do código que faz a requisição com Dio.
Isso nos permite capturar e tratar de forma específica os diferentes tipos de erros que podem surgir durante a execução da aplicação.
Veja como fica o nosso GET utilizando blocos try-catch
:
Future<void> getAllPosts() async {
try {
Response response = await _dio.get('https://jsonplaceholder.typicode.com/posts');
print(response.data.toString());
} catch (erro) {
print(erro.toString());
}
}
Com isso, podemos capturar os erros de forma específica e implementar tratamentos adequados para garantir a estabilidade e confiabilidade das operações de requisição HTTP em nosso aplicativo.
Falando em tratamento de erros com Flutter, que tal dar uma olhada neste artigo sobre Como monitorar erros com Flutter e Firebase Crashlytics aqui da Alura?
Interceptadores
Os interceptadores do Dio atuam como verificadores de requisições e respostas HTTP, modificando e validando dados para garantir segurança e conformidade antes de prosseguir.
Imagine o Dio como a alfândega de um aeroporto. Cada requisição HTTP é uma mala que precisa ser inspecionada.
Os agentes (interceptors) verificam e corrigem o conteúdo na entrada e saída, garantindo que não haja problemas.
Problemas encontrados são resolvidos antes de liberar a mala. Assim, os interceptadores do Dio asseguram que todas as requisições e respostas HTTP sejam seguras e corretas para o aplicativo Flutter.
Vamos ver na prática:
Para uma forma simples de exibir logs das requisições e detalhes das respostas no terminal, podemos adicionar um interceptador ao dio.interceptors
usando o método add
:
dio.interceptors.add(LoggingInterceptor());
Note que o dio.interceptors
mostra uma lista de interceptadores, onde podemos adicionar um novo, porém nesse caso, utilizarmos o interceptador LoggingInterceptor()
fornecido pelo próprio Dio.
Para tratar desses logs em Dio, precisamos criar nossos próprios interceptadores. Vamos começar criando uma classe com um nome significativo, como LoggingInterceptor
. Esta classe vai herdar da classeInterceptor
, que é fornecida pela biblioteca dio.dart
. Veja como fazer isso:
import 'package:dio/dio.dart';
class LoggingInterceptor extends Interceptor {
Como exemplo, utilizaremos três métodos: onRequest
, onResponse
e onError
.
onRequest:
O método onRequest
é chamado antes que uma requisição seja enviada:
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('*** Request ***');
print('URI: ${options.uri}');
print('Method: ${options.method}');
print('Headers: ${options.headers}');
print('Data: ${options.data}');
super.onRequest(options, handler);
}
No código acima, estamos imprimindo no console informações sobre a requisição, incluindo a URI, método HTTP, cabeçalhos e dados da requisição.
onResponse:
O método onResponse
é chamado quando uma resposta é recebida.
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
print('*** Response ***');
print('Status Code: ${response.statusCode}');
print('Data: ${response.data}');
super.onResponse(response, handler);
}
No código acima, estamos imprimindo no console informações sobre a resposta, incluindo o código de status HTTP e os dados da resposta.
onError:
Último mas não menos importante, temos o método onError
, é chamado quando ocorre um erro durante a requisição.
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
print('*** Error ***');
print('Message: ${err.message}');
print('Response: ${err.response}');
super.onError(err, handler);
}
No código acima, estamos imprimindo no console informações sobre o erro, incluindo a mensagem do erro e respostas (se disponível).
Nessa classe LoggingInterceptor
juntamos os três: onRequest
,onResponse
eonError
. Todos úteis para depurar e monitorar o tráfego de rede em sua aplicação Flutter, registrando detalhadamente todas as requisições, respostas e erros.
Você deve ter percebido que em todos os métodos do interceptor estamos usando a anotação@override
. Essa anotação serve para clarificar a intenção de sobrescrita de métodos, nesse contexto do Interceptador, a classe Interceptor
que estamos herdando prevê alguns métodos a serem reescritos, por isso utilizamos o @override
para cada um deles.
Timeout e cancelamento de requisições
O timeout é uma configuração que define quanto tempo uma requisição deve esperar antes de ser abortada caso não haja resposta do servidor, seja por problemas de conexão ou pela própria API.
Pense no timeout como o tempo que você está disposto a esperar por uma pessoa em um encontro. Se essa pessoa não aparecer dentro do tempo combinado, você segue em frente com seu dia.
Da mesma forma, o timeout evita que a aplicação fique presa, aguardando indefinidamente por uma resposta.
Para usar essa funcionalidade em Dio, você pode configurar o timeout da seguinte maneira:
final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: 5000, // 5 segundos
receiveTimeout: 3000, // 3 segundos
));
Nesse código criamos uma instância de Dio para fazer requisições HTTP e definimos tempos máximos de espera. A linha dio.options.connectTimeout = 5000;
estabelece um limite de 5 segundos para conectar ao servidor.
A linha dio.options.receiveTimeout = 3000;
define um limite de 3 segundos para receber uma resposta do servidor. Essas configurações evitam que a aplicação fique travada esperando indefinidamente, melhorando a experiência do usuário.
Cancelamento de Requisições:
O cancelamento de requisições é útil quando você deseja abortar uma requisição que está em andamento, por exemplo, ao sair de uma tela antes que a requisição termine.
Pense nisso como ter a opção de enviar uma mensagem ao seu amigo para cancelar o encontro se algo mais urgente surgir.
Para cancelamento de requisições o Dio tem uma funcionalidade própria para isso chamada CancelToken()
.
É bem útil quando o usuário navega para outra página antes que a resposta da requisição seja recebida ou quando várias requisições são disparadas e apenas a mais recente é necessária. Ou simplesmente quando o usuário aborta a operação.
Veja como implementar no código:
CancelToken cancelToken = CancelToken();
dio.get('https://api.example.com/data', cancelToken: cancelToken).catchError((e) {
if (CancelToken.isCancel(e)) {
print('Requisição cancelada!');
} else {
print('Erro: $e');
}
});
// Para cancelar a requisição
cancelToken.cancel('Operação cancelada pelo usuário.');
Neste exemplo do código acima, criamos um token de cancelamento que vem do pacote dio.dart
, que é usado para cancelar a requisição. Veja que precisamos passar esse cancelToken
como parâmetro do GET, logo após a URL.
Em seguida chamamos o cancelToken.cancel
para cancelar a requisição em andamento, passando uma mensagem para o(a) usuário(a).
Uso avançado com Providers
O Provider é uma biblioteca do Flutter usada para gerenciamento de estado. Imagine que você está em uma cozinha preparando uma refeição. Você tem diversos ingredientes espalhados pela cozinha, e precisa de um sistema eficiente para acessar e utilizar esses ingredientes (dados) em diferentes etapas da receita (componentes do app).
O Provider age como uma despensa organizada, permitindo que você acesse qualquer ingrediente de qualquer parte da cozinha de forma rápida e fácil.
Vamos adotar essa prática em nosso projeto para organizar e centralizar o gerenciamento de estado com o Provider.
Essa abordagem nos permitirá gerenciar de forma eficiente as informações compartilhadas entre diferentes partes da nossa aplicação Flutter.
Definindo um Provider:
Primeiro precisamos definir um Provider através de uma classe que vai estender de ChangeNotifier
, essa classe vai gerenciar esse estado específico da aplicação, algo como:
class DataProvider with ChangeNotifier {
Basicamente, essa classe conterá os métodos necessários para fornecer dados e funcionalidades às diferentes telas da aplicação.
Por exemplo, imagine que já temos o arquivo api_service.dart
com a classe ApiService
, que é responsável pelas configurações do Dio, incluindo os métodos GET, POST, DELETE, PUT, entre outros.
A classe DataProvider
do Provider também terá seus próprios métodos que irão utilizar os métodos do Dio para obter e gerenciar as informações.
Exemplo de método da classe DataProvider
:
Future<void> fetchPosts() async {
_loading = true;
notifyListeners();
try {
_posts = await _apiService.getAllPosts();
} catch (error) {
print('Erro ao buscar posts: $error');
} finally {
_loading = false;
notifyListeners();
}
}
Veja que esse método fetchPosts()
busca o método getAllPosts()
da classe ApiServices
através da instância _apiService.getPosts();
. É importante passarmos o notifyListeners()
para cada método, para notificar os consumidores quando os dados mudarem, garantindo que a interface do usuário seja atualizada de forma reativa.
Fornecendo o Provider na Main:
Agora precisamos fornecer o Provider na main.dart
ou em um widget pai que queira fornecer esse estado gerenciado para a árvore de widgets.
Nesse exemplo utilizaremos a main.dart
mesmo, veja o código a seguir:
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => DataProvider(),
child: MyApp(),
),
);
}
Neste código importamos o ChangeNotifierProvider
do pacote provider.dart
, ele vai ter dois atributos, a função create
que vai criar a instância com nosso provider DataProvider
e o child
que é o nosso aplicativo MyApp()
.
Desta forma,ele vai se tornar um fornecedor da nossa API com Dio para toda árvore do MyApp()
. Utilizamos o underline para ele ignorar o contexto, mas poderia utilizar passando o context
também.
Consumindo o Provider na tela:
Podemos usar o Provider.of<DataProvider>(context)
nos widgets filhos para acessar e usar os dados ou métodos do Provider:
final dataProvider = Provider.of<DataProvider>(context);
Com isso, conseguimos acessar os dados através da final dataProvider
e utilizá-los em nossa tela. Para entender mais sobre como utilizar o Provider no Flutter, confira este artigo: Como gerenciar estados com Flutter Provider.
Autenticação e Autorização com JWT
JSON Web Token (JWT) é como um crachá digital usado após fazer login em um aplicativo, onde você recebe um crachá especial que contém suas informações pessoais, como nome e permissões. Esse crachá é assinado digitalmente para garantir que não seja falsificado.
Cada vez que você precisa acessar diferentes áreas do prédio (fazer uma requisição ao servidor), você mostra seu crachá na porta (envia o JWT no cabeçalho da requisição). Isso evita que você precise se identificar repetidamente, tornando o acesso rápido e seguro.
No Flutter com Dio, configuramos o aplicativo para adicionar automaticamente esse crachá em todas as suas interações com o servidor, facilitando o processo de manter sua autenticação válida enquanto você usa o aplicativo.
Se você ainda não está familiarizado com JSON Web Token (JWT) ou quer entender mais sobre como ele funciona, confira o artigo O que é JSON Web Tokens? para uma explicação detalhada.
Configurando o Dio com JWT:
Vamos começar configurando o Dio para utilizar JWT com interceptadores. O objetivo é garantir que todas as requisições feitas pelo aplicativo incluam automaticamente o token JWT no cabeçalho da requisição, mantendo o usuário autenticado.
Primeiro criamos um Interceptador para adicionar o token JWT nas requisições:
import 'package:dio/dio.dart';
class AuthInterceptor extends Interceptor {
final String? authToken;
AuthInterceptor(this.authToken);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (authToken != null) {
options.headers["Authorization"] = "Bearer $authToken";
}
super.onRequest(options, handler);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
// Aqui você pode lidar com erros de autenticação (como token expirado)
super.onError(err, handler);
}
}
Atualizamos nossa classe ApiService
de serviço da API, onde chamamos os nossos métodos de requisição HTTP, para incluir o AuthInterceptor
:
final Dio _dio = Dio();
String? authToken; // Token JWT para autenticação
ApiService({this.authToken}) {
_dio.interceptors.add(LoggingInterceptor());
_dio.interceptors.add(AuthInterceptor(authToken));
_dio.options.connectTimeout = 5000; // 5 segundos de timeout para conexão
_dio.options.receiveTimeout = 3000; // 3 segundos de timeout para receber a resposta
Atualização do token:
Depois, criaremos um método para atualizar o token JWT no ApiService
:
import 'package:dio_flutter_api/services/api_service.dart';
class AuthService {
final ApiService apiService;
AuthService(this.apiService);
Future<void> login(String username, String password) async {
// Faça a requisição de login e obtenha o token
String token = 'novo_token_obtido_do_servidor';
// Atualize o token no ApiService
apiService.updateAuthToken(token);
}
}
Uso do AuthService:
// Na parte do seu código onde você faz o login
AuthService authService = AuthService(apiService);
await authService.login('usuario', 'senha');
Esse exemplo mostra como integrar autenticação JWT usando Dio e interceptadores no Flutter.
Requisições Multipart/Form-Data com Dio
Em aplicações móveis, é comum a necessidade de enviar dados complexos, como imagens e formulários, para servidores. Uma maneira eficaz de fazer isso é utilizando requisições Multipart/Form-Data. Essa técnica permite enviar esses arquivos em uma única requisição HTTP.
Vamos ver agora como selecionar uma imagem da galeria usando image_picker
e enviá-la a um servidor utilizando o Dio.
Precisamos adicionar a dependência do image_picker
em nosso projeto Flutter:
dependencies:
flutter:
sdk: flutter
dio: ^4.0.0 # Utilize a versão mais recente
image_picker: ^0.8.4+3 # Para selecionar imagens da galeria
Próximo passo é importar essa dependência em nosso projeto:
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:image_picker/image_picker.dart';
Agora sim! Podemos criar uma função para enviar imagem via Multipart/Form-Data, essa função vai chamar enviarImagem()
, como o próprio nome já diz, ela realiza o envio de uma imagem selecionada da galeria do dispositivo para um servidor utilizando o Dio.
É aqui que entra o ImagePicker
para permitir que o(a) usuário(a) selecione uma imagem da galeria:
Future<void> enviarImagem() async {
// Use o ImagePicker para selecionar uma imagem da galeria
final picker = ImagePicker();
final pickedFile = await picker.getImage(source: ImageSource.gallery);
if (pickedFile != null) {
String fileName = pickedFile.path.split('/').last;
// Crie um FormData
FormData formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
pickedFile.path,
filename: fileName,
),
});
// Crie uma instância do Dio e faça a requisição
Dio dio = Dio();
try {
Response response = await dio.post(
'https://api.example.com/upload',
data: formData,
options: Options(
headers: {
'Authorization': 'Bearer your_token_here',
'Content-Type': 'multipart/form-data',
},
),
);
print('Resposta do servidor: ${response.data}');
} catch (e) {
print('Erro ao enviar imagem: $e');
}
}
}
Se uma imagem for selecionada, cria-se um objeto FormData
contendo o arquivo de imagem. Cria-se uma instância de Dio e envia uma requisição POST para o servidor, incluindo a imagem e cabeçalhos necessários, como autorização e tipo de conteúdo. Então, exibe a resposta do servidor ou captura e imprime erros em caso de falha.
Cache de Requisições
Nessa etapa, vamos utilizar o cache para tornar nosso aplicativo Flutter mais rápido e melhorar a experiência do usuário. Optamos por usar o dio_cache_interceptor
para gerenciar o cache das nossas requisições HTTP.
Com ele, conseguimos armazenar as respostas de forma eficiente, o que reduz o tempo de carregamento e alivia a carga no servidor. Podemos utilizar um diretório para armazenar o cache, como o cache do próprio aplicativo.
Como sempre, precisamos adicionar as dependências do Dio e do pacote dio_http_cache
no seu arquivo pubspec.yaml
:
dependencies:
flutter:
sdk: flutter
dio: ^5.0.0 # Utilize a versão mais recente
dio_cache_interceptor: ^5.0.0
Agora podemos importar os pacotes no nosso código, e utilizar o DioCacheInterceptor
para o cache:
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
void main() {
// Crie uma instância do Dio
final dio = Dio();
// Configure o CacheInterceptor
final cacheOptions = CacheOptions(
store: MemCacheStore(), // Armazena o cache na memória
policy: CachePolicy.request, // A política de cache pode ser ajustada
maxStale: Duration(days: 7), // Tempo máximo para considerar o cache como válido
priority: CachePriority.normal,
keyBuilder: CacheOptions.defaultCacheKeyBuilder, // Como gerar a chave do cache
);
dio.interceptors.add(DioCacheInterceptor(options: cacheOptions));
// Utilize o Dio normalmente em sua aplicação
fetchData(dio);
}
Future<void> fetchData(Dio dio) async {
try {
final response = await dio.get('https://jsonplaceholder.typicode.com/posts');
print('Response data: ${response.data}');
} catch (e) {
print('Error fetching data: $e');
}
}
Com essas configurações, sempre que o Dio fizer uma requisição, ele vai primeiro verificar se a resposta já está armazenada no cache.
Se estiver, ele usa essa resposta em vez de fazer uma nova requisição ao servidor. Isso ajuda a tornar o aplicativo muito mais rápido, especialmente quando você precisa da mesma informação várias vezes.
Esse foi um exemplo básico de como usar o cache. Dependendo das necessidades do seu aplicativo, você pode optar por armazenar o cache no disco, para que os dados persistam mesmo após fechar o app, ou ajustar as políticas de cache para controlar melhor quando e como o cache deve ser usado. Assim, você garante que o seu aplicativo oferece a melhor experiência possível aos(às) usuários(às).
Conclusão
Neste artigo, exploramos como o Dio pode facilitar a comunicação com APIs em aplicações Flutter. Vimos desde a instalação e configuração inicial até recursos avançados como tratamento de erros, interceptadores, timeout, cancelamento de requisições e autenticação com JWT.
Também abordamos o uso do Provider para gerenciamento de estado e o envio de dados complexos com Multipart/Form-Data.
Esperamos que este artigo tenha sido útil e que você se sinta mais confiante em utilizar o Dio em seus projetos.
O próximo passo é com você! Experimente o Dio no seu próximo projeto Flutter e veja como ele pode tornar suas requisições HTTP rápidas e fáceis!
Repositório do projeto
Se você quiser ver o código completo e exemplos práticos de como utilizar o Dio com Flutter, visite o repositório do projeto no GitHub. No repositório, você encontrará exemplos aplicados e detalhados dos tópicos abordados neste artigo, como requisições GET e POST, interceptadores, tratamento de erros, e mais.
Conteúdo bônus
Se você está procurando uma IDE incrível para desenvolver com Flutter, o Visual Studio Code (VSCode) é uma excelente opção. Ele é conhecido por sua leveza, extensibilidade e ótimo suporte a plugins.
Com a integração do Flutter DevTools e os plugins Flutter e Dart, o VSCode se adapta às suas necessidades e pode realmente melhorar sua produtividade. Quer saber mais? Confira este artigo sobre Como integrar/rodar o Flutter no VsCode e descubra como tirar o máximo proveito dessa poderosa IDE!
Bons estudos e até a próxima!