Como Integrar Validação de CPF em um App Android com Jetpack Compose

Aprenda a integrar validação de CPF em um app Android com Jetpack Compose. Guia com MVVM, Hilt, formulários e feedback visual em tempo real.

Redação CPFHub.io
Redação CPFHub.io
··8 min de leitura
Como Integrar Validação de CPF em um App Android com Jetpack Compose

Integrar validação de CPF em um app Android com Jetpack Compose significa combinar a reatividade do framework declarativo com chamadas à API do CPFHub.io via coroutines, seguindo o padrão MVVM recomendado pelo Google — o resultado é um formulário que valida o CPF localmente em tempo real e confirma a identidade do usuário via API antes de avançar no fluxo.

Introdução

Jetpack Compose é o toolkit moderno do Android para construir interfaces declarativas em Kotlin. Ao integrar validação de CPF com Compose, você combina a reatividade do framework com chamadas à API do CPFHub.io usando coroutines e o padrão MVVM recomendado pelo Google.

Configurando a injeção de dependências com Hilt

Hilt simplifica a injeção de dependências no Android, garantindo que o serviço de CPF seja criado e gerenciado corretamente.

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import okhttp3.OkHttpClient
import okhttp3.Interceptor

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
    val authInterceptor = Interceptor { chain ->
    val request = chain.request().newBuilder()
    .addHeader("x-api-key", BuildConfig.CPFHUB_API_KEY)
    .build()
    chain.proceed(request)
    }

    return OkHttpClient.Builder()
    .addInterceptor(authInterceptor)
    .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
    .readTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
    .build()
    }

    @Provides
    @Singleton
    fun provideCPFApi(client: OkHttpClient): CPFApi {
    return Retrofit.Builder()
    .baseUrl("https://api.cpfhub.io/")
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    .create(CPFApi::class.java)
    }
}
ComponenteEscopoFunção
NetworkModuleSingletonProvê instâncias únicas de rede
OkHttpClientSingletonCliente HTTP com interceptor de autenticação
CPFApiSingletonInterface Retrofit para a API de CPF
  • BuildConfig.CPFHUB_API_KEY -- chave armazenada no gradle.properties, nunca no código fonte
  • @Singleton -- garante uma única instância do cliente HTTP em toda a aplicação
  • @InstallIn(SingletonComponent) -- o módulo vive durante todo o ciclo de vida do app

Criando o ViewModel com StateFlow

O ViewModel gerencia o estado da tela usando StateFlow, a abordagem recomendada pelo Google para Compose.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

data class CPFScreenState(
    val cpfInput: String = "",
    val resultado: CPFData? = null,
    val erro: String? = null,
    val isLoading: Boolean = false,
    val validacaoLocal: String = ""
)

@HiltViewModel
class CPFViewModel @Inject constructor(
    private val cpfApi: CPFApi
) : ViewModel() {

    private val _state = MutableStateFlow(CPFScreenState())
    val state: StateFlow<CPFScreenState> = _state.asStateFlow()

    fun onCPFChanged(valor: String) {
    val numeros = valor.filter { it.isDigit() }.take(11)
    val validacao = when {
    numeros.length < 11 -> "Faltam ${11 - numeros.length} dígitos"
    validarCPFLocal(numeros) -> "Formato válido"
    else -> "CPF inválido"
    }
    _state.value = _state.value.copy(
    cpfInput = numeros,
    validacaoLocal = validacao,
    erro = null
    )
    }

    fun consultar() {
    viewModelScope.launch {
    _state.value = _state.value.copy(isLoading = true, erro = null, resultado = null)

    try {
    val response = cpfApi.consultarCPF(_state.value.cpfInput)
    if (response.isSuccessful && response.body()?.success == true) {
    _state.value = _state.value.copy(
    resultado = response.body()?.data,
    isLoading = false
    )
    } else {
    _state.value = _state.value.copy(
    erro = "CPF não encontrado",
    isLoading = false
    )
    }
    } catch (e: Exception) {
    _state.value = _state.value.copy(
    erro = "Erro de conexão: ${e.message}",
    isLoading = false
    )
    }
    }
    }

    private fun validarCPFLocal(cpf: String): Boolean {
    if (cpf.length != 11) return false
    val digits = cpf.map { it.digitToInt() }
    if (digits.toSet().size == 1) return false
    val sum1 = (0 until 9).sumOf { digits[it] * (10 - it) }
    val check1 = if (sum1 % 11 < 2) 0 else 11 - (sum1 % 11)
    if (digits[9] != check1) return false
    val sum2 = (0 until 10).sumOf { digits[it] * (11 - it) }
    val check2 = if (sum2 % 11 < 2) 0 else 11 - (sum2 % 11)
    return digits[10] == check2
    }
}
  • StateFlow -- fluxo de estado reativo que Compose coleta automaticamente para recomposição
  • viewModelScope -- escopo de coroutines atrelado ao ciclo de vida do ViewModel
  • copy() -- método das data classes que cria cópias imutáveis com campos alterados

Construindo a tela com Compose

A tela de consulta usa Material 3 e coleta o estado do ViewModel para reagir a mudanças.

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CPFScreen(viewModel: CPFViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsState()

    Scaffold(
    topBar = {
    TopAppBar(title = { Text("Consulta de CPF") })
    }
    ) { padding ->
    Column(
    modifier = Modifier
    .padding(padding)
    .padding(16.dp)
    .fillMaxWidth(),
    verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
    OutlinedTextField(
    value = formatarCPF(state.cpfInput),
    onValueChange = { viewModel.onCPFChanged(it) },
    label = { Text("CPF") },
    placeholder = { Text("000.000.000-00") },
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
    singleLine = true,
    modifier = Modifier.fillMaxWidth(),
    supportingText = {
    Text(
    text = state.validacaoLocal,
    color = if (state.validacaoLocal == "Formato válido")
    MaterialTheme.colorScheme.primary
    else MaterialTheme.colorScheme.onSurfaceVariant
    )
    }
    )

    Button(
    onClick = { viewModel.consultar() },
    enabled = !state.isLoading && state.cpfInput.length == 11,
    modifier = Modifier.fillMaxWidth()
    ) {
    if (state.isLoading) {
    CircularProgressIndicator(
    modifier = Modifier.size(20.dp),
    strokeWidth = 2.dp
    )
    Spacer(modifier = Modifier.width(8.dp))
    }
    Text(if (state.isLoading) "Consultando..." else "Validar CPF")
    }

    state.erro?.let { erro ->
    Card(colors = CardDefaults.cardColors(
    containerColor = MaterialTheme.colorScheme.errorContainer
    )) {
    Text(
    text = erro,
    modifier = Modifier.padding(16.dp),
    color = MaterialTheme.colorScheme.onErrorContainer
    )
    }
    }

    state.resultado?.let { dados ->
    ResultadoCard(dados = dados)
    }
    }
    }
}

@Composable
fun ResultadoCard(dados: CPFData) {
    Card(modifier = Modifier.fillMaxWidth()) {
    Column(modifier = Modifier.padding(16.dp)) {
    Text("Resultado", style = MaterialTheme.typography.titleMedium)
    Spacer(modifier = Modifier.height(8.dp))
    ResultadoRow("Nome", dados.name)
    ResultadoRow("CPF", formatarCPF(dados.cpf))
    ResultadoRow("Nascimento", dados.birthDate)
    ResultadoRow("Sexo", if (dados.gender == "M") "Masculino" else "Feminino")
    }
    }
}

@Composable
fun ResultadoRow(label: String, valor: String) {
    Row(
    modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
    horizontalArrangement = Arrangement.SpaceBetween
    ) {
    Text(label, style = MaterialTheme.typography.bodyMedium)
    Text(valor, style = MaterialTheme.typography.bodyLarge)
    }
}

fun formatarCPF(cpf: String): String {
    val n = cpf.filter { it.isDigit() }
    return buildString {
    n.forEachIndexed { i, c ->
    if (i == 3 || i == 6) append('.')
    if (i == 9) append('-')
    append(c)
    }
    }
}
  • collectAsState() -- converte StateFlow em State do Compose para recomposição automática
  • hiltViewModel() -- obtém o ViewModel com dependências injetadas automaticamente pelo Hilt
  • Material 3 -- componentes com suporte a Dynamic Color e design system moderno

Perguntas frequentes

Como o Jetpack Compose se integra ao padrão MVVM para validação de CPF?

O Compose coleta o StateFlow do ViewModel via collectAsState(), recompondo automaticamente a tela cada vez que o estado muda. O ViewModel fica responsável pela lógica de negócio — validação local e chamada à API — enquanto o Composable cuida apenas da apresentação. Essa separação é recomendada pela documentação oficial do Android e torna o código fácil de testar e manter.

Qual é a diferença entre validação local e consulta à API de CPF?

A validação local verifica os dígitos verificadores matematicamente, sem consumir nenhuma requisição. Ela é instantânea e serve para dar feedback ao usuário antes de submeter o formulário. A consulta à API do CPFHub.io vai além: retorna nome, data de nascimento e gênero do titular, confirmando que o CPF pertence a uma pessoa real — dado essencial para onboarding, KYC e prevenção de fraudes.

Como armazenar a chave de API do CPFHub.io com segurança em um app Android?

Nunca insira a chave diretamente no código-fonte. A abordagem recomendada é armazená-la no gradle.properties (excluído do controle de versão via .gitignore) e acessá-la via BuildConfig. Para aplicações em produção com requisitos de segurança mais altos, considere buscar a chave em um backend próprio, evitando que ela fique embutida no APK.

O que acontece se a API retornar erro ou a conexão falhar durante a validação?

O ViewModel captura exceções via try/catch e atualiza o StateFlow com uma mensagem de erro que o Composable exibe ao usuário. A API do CPFHub.io não bloqueia requisições ao atingir o limite do plano — ela cobra R$0,15 por consulta adicional — portanto erros de rede ou CPF não encontrado são os cenários mais comuns a tratar na interface.


Conclusão

Integrar validação de CPF em um app Android com Jetpack Compose resulta em um código declarativo, reativo e fácil de manter. A combinação de Hilt para injeção de dependências, StateFlow para gerenciamento de estado e Compose para a interface fornece uma arquitetura sólida seguindo as recomendações oficiais do Google. Com validação local em tempo real e consulta à API para confirmação, seu app oferece uma experiência completa ao usuário.

Cadastre-se em cpfhub.io — 50 consultas mensais gratuitas, sem cartão de crédito — e adicione validação de CPF ao seu app Android em menos de 30 minutos.

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