# Como Criar um App Flutter com Validação de CPF em Tempo Real

> Aprenda a criar um app Flutter com validação de CPF em tempo real usando a API do CPFHub.io. Guia com BLoC, formulários e feedback visual.

**Publicado:** 30/12/2024
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/app-flutter-validacao-cpf-tempo-real

---


Para criar um app Flutter com validação de CPF em tempo real, utilize o padrão BLoC para gerenciamento de estado, combine validação local do formato do CPF com uma chamada à API da CPFHub.io para confirmação dos dados cadastrais, e exiba feedback visual progressivo conforme o usuário digita — tudo a partir de um único código Dart que roda em iOS e Android.

## Introdução

Flutter permite construir apps nativos para iOS e Android a partir de um único código Dart. A validação de CPF em tempo real é uma funcionalidade essencial em apps brasileiros, combinando validação local instantânea com consulta à API para confirmação dos dados. Este guia mostra como integrar a API da CPFHub.io usando o padrão BLoC para gerenciamento de estado.

---

## Configurando o projeto e dependências

Comece adicionando as dependências necessárias ao seu `pubspec.yaml`.

```yaml
# pubspec.yaml
dependencies:
 flutter:
 sdk: flutter
 http: ^1.2.0
 flutter_bloc: ^8.1.0
 equatable: ^2.0.5
 flutter_dotenv: ^5.1.0

dev_dependencies:
 flutter_test:
 sdk: flutter
 bloc_test: ^9.1.0
 mocktail: ^1.0.0
```

| Pacote | Versão | Função |
|--------|--------|--------|
| **http** | ^1.2.0 | Requisições HTTP à API |
| **flutter_bloc** | ^8.1.0 | Gerenciamento de estado com BLoC |
| **equatable** | ^2.0.5 | Comparação de estados e eventos |
| **flutter_dotenv** | ^5.1.0 | Variáveis de ambiente para API key |

- **flutter_bloc** -- implementação do padrão BLoC (Business Logic Component) para Flutter
- **equatable** -- permite comparar objetos por valor sem sobrescrever == manualmente
- **flutter_dotenv** -- carrega variáveis de ambiente de um arquivo .env de forma segura

---

## Implementando o BLoC de validação

O BLoC separa a lógica de negócio da interface, recebendo eventos e emitindo estados.

```dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

// Eventos
abstract class CPFEvent extends Equatable {
 @override
 List<Object?> get props => [];
}

class CPFChanged extends CPFEvent {
 final String cpf;
 CPFChanged(this.cpf);

 @override
 List<Object?> get props => [cpf];
}

class CPFSubmitted extends CPFEvent {}

// Estados
abstract class CPFState extends Equatable {
 @override
 List<Object?> get props => [];
}

class CPFInitial extends CPFState {}

class CPFDigitando extends CPFState {
 final String cpf;
 final String feedback;
 final bool formatoValido;

 CPFDigitando({
 required this.cpf,
 required this.feedback,
 required this.formatoValido,
 });

 @override
 List<Object?> get props => [cpf, feedback, formatoValido];
}

class CPFConsultando extends CPFState {}

class CPFSucesso extends CPFState {
 final CPFData dados;
 CPFSucesso(this.dados);

 @override
 List<Object?> get props => [dados];
}

class CPFErro extends CPFState {
 final String mensagem;
 CPFErro(this.mensagem);

 @override
 List<Object?> get props => [mensagem];
}

// BLoC
class CPFBloc extends Bloc<CPFEvent, CPFState> {
 final CPFService _service;

 CPFBloc({required CPFService service})
 : _service = service,
 super(CPFInitial()) {
 on<CPFChanged>(_onChanged);
 on<CPFSubmitted>(_onSubmitted);
 }

 void _onChanged(CPFChanged event, Emitter<CPFState> emit) {
 final numeros = event.cpf.replaceAll(RegExp(r'[^0-9]'), '');
 if (numeros.isEmpty) {
 emit(CPFInitial());
 return;
 }

 final faltam = 11 - numeros.length;
 if (faltam > 0) {
 emit(CPFDigitando(
 cpf: numeros,
 feedback: 'Faltam $faltam dígitos',
 formatoValido: false,
 ));
 } else if (CPFValidator.validar(numeros)) {
 emit(CPFDigitando(
 cpf: numeros,
 feedback: 'CPF válido - pronto para consultar',
 formatoValido: true,
 ));
 } else {
 emit(CPFDigitando(
 cpf: numeros,
 feedback: 'Dígitos verificadores inválidos',
 formatoValido: false,
 ));
 }
 }

 Future<void> _onSubmitted(
 CPFSubmitted event,
 Emitter<CPFState> emit,
 ) async {
 final currentState = state;
 if (currentState is! CPFDigitando || !currentState.formatoValido) return;

 emit(CPFConsultando());

 try {
 final dados = await _service.consultarCPF(currentState.cpf);
 emit(CPFSucesso(dados));
 } on CPFServiceException catch (e) {
 emit(CPFErro(e.message));
 } catch (e) {
 emit(CPFErro('Erro inesperado. Tente novamente.'));
 }
 }
}
```

- **Equatable** -- permite que o BLoC compare estados e evite emissões duplicadas
- **on<Event>** -- registra handlers para cada tipo de evento no construtor do BLoC
- **Emitter** -- interface para emitir novos estados de forma segura dentro dos handlers

---

## Criando o widget de input com máscara

O campo de CPF aplica máscara em tempo real e exibe feedback visual conforme o usuário digita.

```dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class CPFInputWidget extends StatelessWidget {
 const CPFInputWidget({super.key});

 @override
 Widget build(BuildContext context) {
 return BlocBuilder<CPFBloc, CPFState>(
 builder: (context, state) {
 String feedbackText = '';
 Color feedbackColor = Colors.grey;
 bool podeConsultar = false;

 if (state is CPFDigitando) {
 feedbackText = state.feedback;
 feedbackColor = state.formatoValido ? Colors.green : Colors.orange;
 podeConsultar = state.formatoValido;
 }

 return Column(
 crossAxisAlignment: CrossAxisAlignment.stretch,
 children: [
 TextField(
 onChanged: (value) {
 context.read<CPFBloc>().add(CPFChanged(value));
 },
 keyboardType: TextInputType.number,
 inputFormatters: [
 FilteringTextInputFormatter.digitsOnly,
 LengthLimitingTextInputFormatter(11),
 _CPFInputFormatter(),
 ],
 decoration: InputDecoration(
 labelText: 'CPF',
 hintText: '000.000.000-00',
 border: const OutlineInputBorder(),
 helperText: feedbackText,
 helperStyle: TextStyle(color: feedbackColor),
 suffixIcon: state is CPFConsultando
 ? const SizedBox(
 width: 20,
 height: 20,
 child: CircularProgressIndicator(strokeWidth: 2),
 )
 : null,
 ),
 ),
 const SizedBox(height: 16),
 ElevatedButton(
 onPressed: podeConsultar
 ? () => context.read<CPFBloc>().add(CPFSubmitted())
 : null,
 style: ElevatedButton.styleFrom(
 padding: const EdgeInsets.symmetric(vertical: 16),
 ),
 child: Text(
 state is CPFConsultando ? 'Consultando...' : 'Validar CPF',
 ),
 ),
 ],
 );
 },
 );
 }
}

class _CPFInputFormatter extends TextInputFormatter {
 @override
 TextEditingValue formatEditUpdate(
 TextEditingValue oldValue,
 TextEditingValue newValue,
 ) {
 final digits = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
 final buffer = StringBuffer();

 for (int i = 0; i < digits.length && i < 11; i++) {
 if (i == 3 || i == 6) buffer.write('.');
 if (i == 9) buffer.write('-');
 buffer.write(digits[i]);
 }

 return TextEditingValue(
 text: buffer.toString(),
 selection: TextSelection.collapsed(offset: buffer.length),
 );
 }
}
```

- **BlocBuilder** -- widget que reconstrói a UI automaticamente quando o estado do BLoC muda
- **TextInputFormatter** -- permite interceptar e modificar o texto durante a digitação
- **context.read** -- acessa o BLoC sem escutar mudanças, ideal para disparar eventos

---

## Montando a tela completa com exibição de resultados

A tela principal combina o input, resultados e tratamento de erro em um layout responsivo.

```dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

class CPFPage extends StatelessWidget {
 const CPFPage({super.key});

 @override
 Widget build(BuildContext context) {
 return BlocProvider(
 create: (_) => CPFBloc(
 service: CPFService(apiKey: dotenv.env['CPFHUB_API_KEY'] ?? ''),
 ),
 child: Scaffold(
 appBar: AppBar(title: const Text('Consulta de CPF')),
 body: SingleChildScrollView(
 padding: const EdgeInsets.all(16),
 child: Column(
 children: [
 const CPFInputWidget(),
 const SizedBox(height: 24),
 BlocBuilder<CPFBloc, CPFState>(
 builder: (context, state) {
 if (state is CPFSucesso) {
 return _buildResultado(state.dados);
 }
 if (state is CPFErro) {
 return _buildErro(state.mensagem);
 }
 return const SizedBox.shrink();
 },
 ),
 ],
 ),
 ),
 ),
 );
 }

 Widget _buildResultado(CPFData dados) {
 return Card(
 child: Padding(
 padding: const EdgeInsets.all(16),
 child: Column(
 crossAxisAlignment: CrossAxisAlignment.start,
 children: [
 const Text(
 'Resultado da Consulta',
 style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
 ),
 const Divider(),
 _infoRow('Nome', dados.name),
 _infoRow('CPF', CPFValidator.formatar(dados.cpf)),
 _infoRow('Nascimento', dados.birthDate),
 _infoRow('Sexo', dados.gender == 'M' ? 'Masculino' : 'Feminino'),
 ],
 ),
 ),
 );
 }

 Widget _infoRow(String label, String valor) {
 return Padding(
 padding: const EdgeInsets.symmetric(vertical: 4),
 child: Row(
 mainAxisAlignment: MainAxisAlignment.spaceBetween,
 children: [
 Text(label, style: const TextStyle(fontWeight: FontWeight.w500)),
 Text(valor),
 ],
 ),
 );
 }

 Widget _buildErro(String mensagem) {
 return Card(
 color: Colors.red.shade50,
 child: Padding(
 padding: const EdgeInsets.all(16),
 child: Row(
 children: [
 Icon(Icons.error_outline, color: Colors.red.shade700),
 const SizedBox(width: 12),
 Expanded(
 child: Text(mensagem, style: TextStyle(color: Colors.red.shade700)),
 ),
 ],
 ),
 ),
 );
 }
}
```

- **BlocProvider** -- injeta o BLoC na árvore de widgets para acesso pelos filhos
- **dotenv.env** -- acessa a API key de forma segura sem hardcode no código
- **SingleChildScrollView** -- permite scroll quando o conteúdo excede a tela

---

## Perguntas frequentes

### Por que usar o padrão BLoC para validação de CPF em Flutter?
O padrão BLoC separa completamente a lógica de negócio da interface: o widget de input dispara eventos (CPFChanged, CPFSubmitted), o BLoC processa a validação local e a chamada à API, e os estados resultantes (CPFDigitando, CPFConsultando, CPFSucesso, CPFErro) são refletidos na UI automaticamente. Isso facilita testes unitários — é possível testar o BLoC sem inicializar nenhum widget Flutter. A documentação oficial do [Dart](https://dart.dev/guides) detalha os conceitos de streams que fundamentam o padrão.

### Como a validação local difere da consulta à API no app Flutter?
A validação local verifica apenas se o CPF tem 11 dígitos e se os dígitos verificadores são matematicamente corretos — isso é feito instantaneamente, sem chamada de rede. A consulta à API vai além: confirma se o CPF existe na base cadastral e retorna o nome completo e a data de nascimento do titular. A sequência correta é validar localmente primeiro e só chamar a API quando o formato estiver correto, evitando requisições desnecessárias.

### Como armazenar a API key com segurança em um app Flutter?
Use o pacote `flutter_dotenv` para carregar a chave de um arquivo `.env` que não deve ser commitado no repositório. Em produção, prefira um backend intermediário que faz a chamada à API da CPFHub.io: o app móvel chama o seu próprio servidor, que guarda a `x-api-key` em variáveis de ambiente seguras, sem expô-la no bundle do app. Esse padrão evita que a chave apareça em ferramentas de análise de APK ou IPA.

### A API CPFHub.io bloqueia requisições se o limite do plano for atingido?
Não. A API CPFHub.io nunca retorna HTTP 429 nem bloqueia requisições. Ao ultrapassar as 50 consultas mensais do plano gratuito (ou as 1.000 do plano Pro), cada consulta adicional é cobrada a R$0,15 — o fluxo do app continua funcionando normalmente. Gerencie o consumo acompanhando o uso em [app.cpfhub.io/settings/billing](https://app.cpfhub.io/settings/billing).

### Leia também

- [Como consumir API de CPF em Flutter com Dart e pacote http](https://cpfhub.io/blog/como-consumir-api-de-cpf-em-flutter-com-dart-e-pacote-http)
- [Como consumir API de CPF em React Native para apps mobile nativos](https://cpfhub.io/blog/como-consumir-api-cpf-react-native-apps-mobile-nativos)
- [Como consumir a API de CPF em Dart usando http package](https://cpfhub.io/blog/consumir-api-cpf-dart-http)
- [Mobile-first: como otimizar a validação de CPF em dispositivos móveis](https://cpfhub.io/blog/mobile-first-como-otimizar-a-validacao-de-cpf-em-dispositivos-moveis)

---

## Conclusão

Construir um app Flutter com validação de CPF em tempo real é uma tarefa estruturada quando se utiliza o padrão BLoC para gerenciamento de estado. A separação entre eventos, estados e lógica de negócio resulta em código testável e manutenível. A combinação de máscara de input, validação progressiva e consulta à API oferece uma experiência completa ao usuário brasileiro.

Cadastre-se em [cpfhub.io](https://www.cpfhub.io/) — 50 consultas mensais gratuitas, sem cartão de crédito — e integre a validação de CPF ao seu app Flutter ainda hoje, com exemplos de código prontos para usar em Dart.

