Publicar um app Android é só o começo. O desafio real aparece quando milhares de aparelhos, versões do Android, fabricantes, redes móveis, estados de login e fluxos de usuário começam a rodar o mesmo código em produção. Um crash que nunca apareceu no emulador pode afetar apenas Android 13 em um modelo específico. Um ANR pode surgir só quando o usuário abre o app com banco local grande. Uma falha de sincronização pode acontecer apenas depois de alternar entre offline e online várias vezes. Sem uma estratégia de observabilidade mobile, o time descobre tudo isso tarde demais.

É nesse ponto que Firebase Crashlytics com Kotlin vira uma ferramenta prática, não apenas um painel bonito. Crashlytics coleta crashes, ANRs, stack traces, versão do app, modelo do dispositivo, sistema operacional, logs e chaves customizadas. Usado com disciplina, ele ajuda a responder três perguntas essenciais: o que quebrou, quem foi afetado e qual mudança provavelmente causou o problema.

Este guia mostra como instrumentar Crashlytics em apps Android com Kotlin, como registrar contexto útil sem vazar dados sensíveis, como tratar ANRs, como conectar releases e como transformar relatórios em um processo de estabilidade. Se você ainda está montando a base do app, leia também a trilha de Android offline-first com Kotlin, o guia de testes Android com Compose e Maestro e o artigo sobre segurança de dados locais no Android.

O que Crashlytics resolve de verdade?

Crashlytics não impede bug. Ele encurta o caminho entre falha real e correção confiável. Em vez de depender de relato manual do usuário, print do WhatsApp ou comentário genérico na Play Store, você passa a receber grupos de falha com stack trace, frequência, versões afetadas, dispositivos, breadcrumbs e sinais de regressão.

Na prática, ele ajuda em cenários como:

  • crash em ViewModel depois de uma resposta inesperada da API;
  • NullPointerException em tela Compose por estado parcialmente carregado;
  • erro de migration do Room em usuários que atualizaram de versões antigas;
  • falha de desserialização em Retrofit ou Ktor Client;
  • ANR causado por I/O, banco local ou JSON pesado na main thread;
  • crash em worker de sincronização executado em background;
  • instabilidade depois de ativar uma feature flag ou rollout gradual.

O ponto importante é que Crashlytics não substitui testes. Ele complementa. Testes pegam cenários previstos antes do release. Crashlytics mostra o que escapou em aparelhos reais.

Configuração básica com Gradle Kotlin DSL

Em um projeto Android moderno, a configuração costuma envolver o plugin do Google Services, o plugin do Crashlytics e as dependências do Firebase BoM. No settings.gradle.kts ou no bloco de plugins raiz, você declara as versões compatíveis com seu projeto:

plugins {
    id("com.android.application") version "8.7.0" apply false
    id("org.jetbrains.kotlin.android") version "2.0.21" apply false
    id("com.google.gms.google-services") version "4.4.2" apply false
    id("com.google.firebase.crashlytics") version "3.0.2" apply false
}

No módulo do app:

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.gms.google-services")
    id("com.google.firebase.crashlytics")
}

dependencies {
    implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
    implementation("com.google.firebase:firebase-crashlytics-ktx")
    implementation("com.google.firebase:firebase-analytics-ktx")
}

Use sempre versões atuais e compatíveis com Android Gradle Plugin, Kotlin e o restante do app. Em projetos grandes, vale centralizar versões em libs.versions.toml, como explicado no conteúdo sobre Gradle Version Catalog em Kotlin.

Também é necessário adicionar o arquivo google-services.json do projeto Firebase ao módulo correto. Esse arquivo identifica o app, mas não deve ser tratado como segredo de servidor. Mesmo assim, revise o processo de configuração para evitar misturar ambientes de desenvolvimento, homologação e produção.

Ativando coleta com controle por ambiente

Nem todo build precisa enviar crash para o painel de produção. Builds locais e de QA podem poluir métricas se usarem o mesmo app Firebase. Uma abordagem simples é separar projetos Firebase por ambiente ou controlar a coleta conforme BuildConfig.

class KotlinBrasilApp : Application() {
    override fun onCreate() {
        super.onCreate()

        FirebaseCrashlytics.getInstance()
            .setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
    }
}

Em times maiores, prefira uma regra explícita por flavor:

android {
    productFlavors {
        create("dev") {
            buildConfigField("boolean", "CRASHLYTICS_ENABLED", "false")
        }
        create("prod") {
            buildConfigField("boolean", "CRASHLYTICS_ENABLED", "true")
        }
    }
}

Depois, use BuildConfig.CRASHLYTICS_ENABLED na inicialização. Isso evita que uma build interna com dados artificiais pareça um incidente real.

Logs úteis sem vazar dados sensíveis

Crashlytics permite registrar logs que aparecem junto do relatório de crash. Isso é útil para reconstruir os passos finais antes da falha, mas também perigoso se o time registrar payloads completos, tokens, emails, CPF, endereço ou mensagens privadas.

Prefira logs de evento e estado resumido:

private val crashlytics = FirebaseCrashlytics.getInstance()

fun abrirDetalhePedido(idPedido: String, origem: String) {
    crashlytics.log("abrir_detalhe_pedido")
    crashlytics.setCustomKey("origem_tela", origem)
    crashlytics.setCustomKey("pedido_id_hash", idPedido.sha256Curto())

    // Carrega dados da tela...
}

O exemplo usa um hash curto do identificador, não o ID real. A regra é simples: o log deve ajudar engenharia a investigar sem transformar Crashlytics em uma cópia paralela do banco do usuário.

Registre informações como:

  • tela ou fluxo atual;
  • versão da feature flag;
  • estado resumido de login, sem token;
  • tipo de rede, sem localização precisa;
  • tamanho da fila offline;
  • versão do schema local;
  • resultado de uma migration;
  • nome lógico da operação, não payload completo.

Não registre:

  • access token ou refresh token;
  • senha, CPF, cartão, telefone ou endereço;
  • corpo completo de resposta da API;
  • mensagens privadas;
  • coordenadas de localização sem necessidade real;
  • dados de saúde, financeiros ou corporativos identificáveis.

Esse cuidado conversa diretamente com segurança de dados locais no Android. Observabilidade sem privacidade vira risco.

Chaves customizadas que realmente ajudam

Custom keys são campos anexados ao relatório de crash. Elas ajudam a segmentar falhas por contexto. O erro comum é criar dezenas de chaves sem padrão, com valores livres e difíceis de buscar.

Boas chaves para apps Android com Kotlin:

crashlytics.setCustomKey("screen", "checkout_review")
crashlytics.setCustomKey("auth_state", "logged_in")
crashlytics.setCustomKey("network_type", "cellular")
crashlytics.setCustomKey("offline_queue_size", syncQueue.count())
crashlytics.setCustomKey("room_schema_version", 12)
crashlytics.setCustomKey("feature_new_checkout", true)
crashlytics.setCustomKey("compose_enabled", true)

Escolha nomes estáveis, em inglês ou português, mas não misture sem critério. Evite valores de alta cardinalidade, como ID único de pedido, email ou URL completa. Chave customizada boa serve para agrupar investigação. Se cada usuário gera um valor diferente, a busca perde força.

Erros não fatais: quando registrar exception sem crash

Nem todo erro derruba o app. Uma API pode falhar e mostrar retry. Uma fila offline pode adiar sincronização. Uma imagem pode não carregar. Esses casos não devem virar crash artificial, mas alguns merecem registro como erro não fatal quando indicam problema de produto ou regressão.

suspend fun sincronizar() {
    try {
        repository.sincronizarPendentes()
    } catch (e: HttpException) {
        if (e.code() >= 500) {
            FirebaseCrashlytics.getInstance().recordException(e)
        }
        throw e
    } catch (e: SQLiteException) {
        FirebaseCrashlytics.getInstance().recordException(e)
        throw e
    }
}

Use esse recurso com moderação. Se você registra toda falha de rede como exception, o painel vira ruído. Bons candidatos para não fatais:

  • erro de migration do banco local recuperado automaticamente;
  • falha repetida de sincronização em background;
  • contrato de API inesperado;
  • exceção em integração de pagamento recuperada com fallback;
  • estado impossível detectado por regra de domínio;
  • falha em feature flag crítica.

Para chamadas HTTP comuns, prefira métricas agregadas e logs estruturados. Crashlytics deve destacar aquilo que precisa de investigação, não substituir analytics.

ANR: o crash silencioso que prejudica retenção

ANR significa Application Not Responding. O Android mostra esse erro quando a main thread fica bloqueada tempo demais. Para o usuário, a sensação é pior do que um crash rápido: o app congela, não responde ao toque e parece pesado.

Em apps Kotlin, causas comuns de ANR incluem:

  • chamada de rede fora de coroutine adequada;
  • leitura pesada de arquivo na main thread;
  • query grande no Room sem dispatcher correto;
  • parsing JSON pesado durante composição da tela;
  • loop caro dentro de composable;
  • inicialização excessiva no Application.onCreate();
  • bloqueio com runBlocking em código de produção;
  • sincronização de dados grandes durante abertura da tela.

Crashlytics ajuda a agrupar ANRs e mostrar stack traces, mas a correção depende de arquitetura. Trabalho pesado deve sair da main thread, telas Compose precisam observar estado já preparado e operações de I/O devem passar por repositories com coroutines bem definidas.

Um padrão básico:

class PerfilRepository(
    private val dao: PerfilDao,
    private val api: PerfilApi,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
    suspend fun atualizarPerfil(): Perfil = withContext(ioDispatcher) {
        val remoto = api.buscarPerfil()
        dao.salvar(remoto.toEntity())
        remoto.toDomain()
    }
}

Isso parece simples, mas previne uma classe inteira de problemas. Se o app já usa Room, Retrofit e WorkManager, defina fronteiras claras: ViewModel coordena estado, repository decide dados, dispatcher protege I/O e worker cuida do trabalho adiado.

Compose: cuidado com recomposição e estado pesado

Jetpack Compose melhora muito a ergonomia de UI, mas também muda a forma de investigar travamentos. Uma tela pode parecer correta em testes simples e ainda sofrer com recomposição excessiva, listas sem key, lambdas criando objetos pesados ou derivação de estado dentro do composable.

Evite fazer trabalho caro diretamente na UI:

@Composable
fun PedidosScreen(state: PedidosState) {
    // Evite filtrar e ordenar listas grandes aqui a cada recomposição.
    val pedidos = state.pedidos
        .filter { it.status == Status.PENDENTE }
        .sortedByDescending { it.criadoEm }

    LazyColumn {
        items(pedidos) { pedido ->
            PedidoItem(pedido)
        }
    }
}

Prefira preparar a lista no ViewModel ou usar estado derivado com critério quando o custo justificar:

data class PedidosState(
    val pedidosPendentesOrdenados: List<PedidoUi>,
    val carregando: Boolean,
)

Crashlytics pode apontar o ANR, mas a prevenção vem de modelar estado de UI com responsabilidade. Para aprofundar a base, veja o guia de Jetpack Compose e a trilha de testes Android com Kotlin.

Release tracking e regressões

Crashlytics ganha muito valor quando cada relatório é ligado a uma versão de app, build number e commit. Sem isso, o time sabe que algo quebrou, mas não sabe quando entrou.

No Android, mantenha versionName e versionCode consistentes. Em CI/CD, gere build com metadados claros:

android {
    defaultConfig {
        versionCode = 12043
        versionName = "2.14.3"
    }
}

Se o time usa distribuição gradual, acompanhe crashes por versão antes de ampliar rollout. Uma versão com crash-free users abaixo do padrão deve pausar. O mesmo vale para ANR rate. Não trate “não recebi reclamação” como sucesso; usuários simplesmente desinstalam apps instáveis.

Esse ciclo combina bem com CI/CD para Kotlin e feature flags. Deploy seguro não é só build verde; é observar a versão depois que ela chega a aparelhos reais.

WorkManager e falhas em background

Muitos crashes de app Android moderno não acontecem com uma tela aberta. Eles aparecem em workers: sync offline, upload de foto, limpeza de cache, refresh de dados, envio de evento ou tarefa periódica.

Ao instrumentar workers, registre contexto antes da operação crítica:

class SyncWorker(
    context: Context,
    params: WorkerParameters,
    private val syncRepository: SyncRepository,
) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        val crashlytics = FirebaseCrashlytics.getInstance()
        crashlytics.setCustomKey("worker", "sync")
        crashlytics.setCustomKey("attempt", runAttemptCount)

        return try {
            syncRepository.sincronizar()
            Result.success()
        } catch (e: IOException) {
            Result.retry()
        } catch (e: Exception) {
            crashlytics.recordException(e)
            Result.failure()
        }
    }
}

Não envie exception para toda falha transitória. IOException por rede instável é parte normal de um app offline-first. Registre quando o erro indica bug, contrato inválido, estado impossível ou falha persistente depois de várias tentativas.

Processo de triagem: do painel ao pull request

Ferramenta nenhuma resolve estabilidade se o time não cria rotina. Uma boa triagem semanal ou por release pode seguir este fluxo:

  1. filtrar crashes e ANRs por versão atual;
  2. priorizar falhas com crescimento recente, usuários afetados ou fluxo crítico;
  3. identificar se o grupo começou depois de um release específico;
  4. reproduzir com estado parecido, device semelhante ou dados locais simulados;
  5. escrever teste de regressão quando possível;
  6. corrigir e marcar o issue como resolvido apenas depois do release;
  7. acompanhar se o grupo reabre em versões futuras.

O erro comum é tratar Crashlytics como caixa de entrada infinita. Nem todo grupo tem o mesmo impacto. Um crash raro em uma tela experimental pesa menos que ANR recorrente no login, checkout, pagamento, busca ou sincronização inicial.

Integração com suporte e produto

Crashlytics também ajuda fora da engenharia, desde que você defina uma linguagem comum. Suporte pode informar versão do app, modelo do aparelho e horário aproximado do problema. Produto pode sinalizar fluxo crítico. Engenharia cruza isso com grupos de falha.

Evite pedir ao usuário dados sensíveis ou logs manuais quando o app pode registrar contexto seguro automaticamente. Por outro lado, não transforme cada relatório em vigilância. O objetivo é estabilidade, não coleta excessiva.

Para times brasileiros com apps em fintech, varejo, saúde, educação, logística ou delivery, essa maturidade pesa em carreira. Vagas Android frequentemente citam Firebase, Crashlytics, CI/CD, testes, analytics, performance e arquitetura. Saber explicar como você investiga falhas reais é tão importante quanto saber montar uma tela Compose.

Erros comuns com Crashlytics

Alguns problemas aparecem repetidamente:

  • usar o mesmo projeto Firebase para dev e produção: polui métricas e confunde prioridade;
  • registrar dados sensíveis em logs: transforma observabilidade em risco de privacidade;
  • ignorar ANRs: app congelando também destrói experiência;
  • registrar toda falha de rede como exception: cria ruído e esconde problemas reais;
  • não ligar falha a versão: dificulta achar regressão;
  • depender só do stack trace: sem chaves customizadas, falta contexto;
  • marcar issue como resolvido cedo demais: espere o release chegar ao usuário;
  • não escrever teste depois da correção: o mesmo bug volta em outra forma.

Crashlytics é mais efetivo quando entra no processo de engenharia, não quando alguém abre o painel só depois de uma crise.

Checklist para produção

Antes de considerar a observabilidade mobile pronta, revise:

  • coleta habilitada corretamente apenas nos ambientes desejados;
  • versionName e versionCode consistentes por release;
  • logs sem tokens, documentos, emails ou payloads sensíveis;
  • custom keys padronizadas para tela, feature, fluxo e estado relevante;
  • estratégia para não fatais com limite de ruído;
  • triagem regular de crashes e ANRs;
  • alertas para regressões em versão nova;
  • testes de regressão para falhas importantes;
  • documentação curta explicando como suporte deve reportar problemas;
  • revisão de privacidade alinhada ao tipo de dado do app.

Se você trabalha com backend Kotlin também, vale conectar essa mentalidade ao conteúdo de observabilidade em Kotlin. No mobile, a diferença é que o ambiente do usuário é muito mais variável; por isso, contexto seguro e versões bem rastreadas fazem tanta diferença.

Conclusão

Firebase Crashlytics com Kotlin é uma das formas mais pragmáticas de melhorar estabilidade de apps Android em produção. Ele mostra crashes, ANRs e exceções não fatais em aparelhos reais, com contexto suficiente para transformar falhas dispersas em correções priorizadas. Mas o valor não vem só da dependência no Gradle. Vem de instrumentação cuidadosa, logs seguros, chaves customizadas, versionamento, triagem e conexão com testes.

Um app Android maduro não é aquele que nunca falha. É aquele que detecta falhas rapidamente, protege dados do usuário, aprende com incidentes e reduz regressões a cada release. Para continuar evoluindo a base, combine Crashlytics com WorkManager, Android offline-first, testes Android e CI/CD para Kotlin. Esse conjunto aproxima seu app do padrão que empresas esperam de Android Kotlin em 2026.