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.
# 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.
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.
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.
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 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.
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 — 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.
CPFHub.io
Pronto para integrar a API?
50 consultas gratuitas para testar agora. Sem cartão de crédito. Acesso imediato à documentação.
Sobre a redação
Redação CPFHub.io
Time editorial especializado em APIs de CPF, identidade digital e compliance no mercado brasileiro. Produzimos guias técnicos, análises regulatórias e tutoriais sobre LGPD e KYC para desenvolvedores e líderes de produto.



