Interface do Usuário Otimista e Estados de Pagamento Offline no Flutter: Lidando com Falhas na Infraestrutura do Backend
No meu artigo anterior, exploramos como sistemas de backend projetam motores de pagamento rail-agnosticos para sobreviver a interrupções na infraestrutura, como a negação da confiança do banco Wise nos EUA.
Mas a redundância de backend é apenas metade da batalha.
O que acontece na tela do cliente quando uma linha de pagamento estremece, tempos out ou dispara um fallback dinâmico? Se seu aplicativo Flutter depende de carregadores giratórios e chamadas rígidas await para cada solicitação da rede, a perda de uma API bancária cria uma experiência do usuário perturbadora ou pior, cobranças duplicadas.
Como engenheiros de produto, devemos projetar arquiteturas de estado móvel que permanecem calmas quando as linhas do backend estacionam. Aqui está como implementar atualizações otimistas da interface do usuário, filas de transações offline e reversões graciosas de estado no Flutter usando Riverpod.
O Problema Central: O Trabalho de UX Sinônimo
Os fluxos tradicionais de pagamento móvel geralmente parecem assim:
- O usuário toca “Pagar $50”.
- Mostrar um indicador de progresso circular cheio na tela.
- Esperar 3–8 segundos para que o backend atinja a gateway de pagamento.
- Se o servidor estiver fora do ar ou falhar ao se conectar a uma linha secundária, a interface do usuário congela ou exibe um snackbar vermelho genérico.
Este método falha com usuários móveis em conexões ruins ou durante pivôs de linha no lado do servidor.
Em vez disso, um aplicativo fintech de produção deve adotar uma mentalidade Otimista Primeiro:
1. Modelando o Estado Otimista com Riverpod
Para prevenir flashes na interface do usuário e lidar com a reconciliação assíncrona, seu objeto de estado precisa representar confiança local separada da confirmação do backend.
Aqui está um modelo de estado limpo usando Notifier no Riverpod:
enum StatusPagamento { pendente, concluido, falha }
class EstadoTransacao {
final String id;
final double valor;
final StatusPagamento status;
final bool otimista;
final String? mensagemErro;
EstadoTransacao({
required this.id,
required this.valor,
required this.status,
this.otimista = false,
this.mensagemErro,
});
EstadoTransacao copiaCom({
StatusPagamento? status,
bool? otimista,
String? mensagemErro,
}) {
return EstadoTransacao(
id: this.id,
valor: this.valor,
status: status ?? this.status,
otimista: otimista ?? this.otimista,
mensagemErro: mensagemErro ?? this.mensagemErro,
);
}
}2. Implementando Mutação Otimista e Rollback
Quando o usuário dispara um pagamento, nós imediatamente injetamos a nova transação em nosso estado local com otimista = true e atualizamos o saldo imediatamente.
Se o backend falhar — mesmo após tentar novamente ou mudar de rota — nós executamos um rollback atômico para restaurar o estado anterior do usuário e informá-los sem quebrar o contexto da interface do usuário.
@riverpod
class NotificadorPagamento extends _$NotificadorPagamento {
@override
List<EstadoTransacao> build() => [];
Future<void> enviarPagamento({required String id, required double valor}) async {
// 1. Snapshot do estado anterior para segurança de rollback
final estadoAnterior = state;
// 2. Atualização local otimista (feedback instantâneo)
final transacaoOtimista = EstadoTransacao(
id: id,
valor: valor,
status: StatusPagamento.pendente,
otimista: true,
);
state = [transacaoOtimista, ...state];
try {
// 3. Enviar para API do backend
final resposta = await ref.read(pagamentoRepositoryProvider).executarTransferencia(
id: id,
valor: valor,
);
// 4. Em caso de sucesso: reconciliar com a resposta real do servidor
state = state.map((tx) {
if (tx.id == id) {
return tx.copiaCom(
status: StatusPagamento.concluido,
otimista: false,
);
}
return tx;
}).toList();
} catch (e) {
// 5. Em caso de falha: roll back do estado otimista com elegância
state = estadoAnterior;
// Notificar a interface do usuário via efeito lateral / canal de eventos
ref.read(notificationStreamProvider).add("Pagamento falhou: ${e.toString()}");
}
}
}3. Persistência Offline: Caching Local com Hive ou Isar
E se o dispositivo perder conexão exatamente quando o usuário envia um pagamento?
Mais do que falhar imediatamente, aplicativos móveis resilientes armazenam transações pendentes localmente em um motor de armazenamento persistente offline (como Hive ou Isar) antes de tentar a transmissão de rede.
- Escriva Local: Comite o payload da transação para o armazenamento local do dispositivo com uma
idempotencyKey(UUID v4). - Sincronização em segundo plano: Use um ouvinte de conectividade ou trabalhador em segundo plano para esvaziar a fila offline assim que a disponibilidade da rede retornar.
- Certeza de Idempotência: Como o cliente gera a
idempotencyKeyantecipadamente, reenviar uma transação offline na fila não irá duplicar a cobrança ao usuário, mesmo que a linha do backend tenha experimentado um temporário timeout.
Tomas Chave para Engenheiros de Flutter
Lidar com volatilidade da linha de trás no cliente se resume a três princípios:
- Feedback UI Instantâneo: Muta o estado local imediatamente. Não faça o usuário esperar pela confirmação de assentamento bancário externo para ver progresso visual.
- Sempre Mantenha uma Rota de Desfazer: Nunca muta o estado irreversivelmente sem manter um snapshot para restaurar se a chamada à rede falhar.
- A Idempotência é Gerenciada pelo Cliente: Gere chaves únicas no dispositivo móvel antes de enviar a solicitação. Isso permite reenvios seguros em redes instáveis sem o risco de cobranças duplicadas.
Quando você combina um backend agnóstico da linha com um cliente móvel otimista e resiliente, seu aplicativo se sente rápido como a luz e sólido — independentemente do que acontece atrás da porta de entrada da API.

