# 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.

**Publicado:** 24/12/2024
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/validacao-cpf-android-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.

```kotlin
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)
 }
}
```

| Componente | Escopo | Função |
|-----------|--------|--------|
| **NetworkModule** | Singleton | Provê instâncias únicas de rede |
| **OkHttpClient** | Singleton | Cliente HTTP com interceptor de autenticação |
| **CPFApi** | Singleton | Interface 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.

```kotlin
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.

```kotlin
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](https://developer.android.com/topic/architecture) 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.

### Leia também

- [Como consumir API de CPF em Kotlin para aplicações Android nativas](https://cpfhub.io/blog/como-consumir-api-de-cpf-em-kotlin-para-aplicacoes-android-nativas)
- [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 Kotlin Usando Ktor Client e Retrofit](https://cpfhub.io/blog/consumir-api-cpf-kotlin-ktor-retrofit)
- [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

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](https://www.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.

