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.

Redação CPFHub.io
Redação CPFHub.io
··8 min de leitura
Como Criar um App Flutter com Validação de CPF em 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.

# 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
PacoteVersãoFunção
http^1.2.0Requisições HTTP à API
flutter_bloc^8.1.0Gerenciamento de estado com BLoC
equatable^2.0.5Comparação de estados e eventos
flutter_dotenv^5.1.0Variá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.

Redação CPFHub.io

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.

WhatsAppFale conosco via WhatsApp