Entrevistas de Android focam nos idioms do Kotlin, nas coroutines e no ciclo de vida da plataforma — a maioria das perguntas de nível sênior está, na verdade, testando se você entende que o seu processo pode ser encerrado a qualquer momento. Abaixo estão as perguntas mais feitas, cada uma com uma resposta modelo.
Cada pergunta vem com uma resposta modelo para você comparar com a sua.
1
Como funciona a null safety do Kotlin, e quando um platform type é perigoso?
Resposta
O Kotlin separa cada tipo em nullable (String?) e non-nullable (String), e o compilador se recusa a desreferenciar um valor nullable sem uma verificação, então o NullPointerException praticamente desaparece. Platform types surgem na fronteira com o Java: o Kotlin não sabe se um método Java pode retornar null, então confia em você. É daí que ainda vêm os NPEs — anote as APIs Java ou trate o resultado delas como nullable no local da chamada.
2
O que data classes e sealed classes te dão?
Resposta
Uma data class gera equals, hashCode, toString, copy e destructuring a partir do construtor primário, o que a torna a ferramenta certa para valores que você compara e copia — modelos de API, estado de UI. Uma sealed class restringe suas subclasses a um conjunto conhecido, então um when sobre ela é exaustivo e o compilador aponta qualquer branch esquecido. Juntas, elas modelam o estado de UI de forma limpa: uma sealed interface para Loading, Content, Error, com data classes carregando o payload.
3
O que é concorrência estruturada em coroutines, e o que um scope faz?
Resposta
Concorrência estruturada significa que toda coroutine pertence a um scope e a um job pai, então cancelar o pai cancela todos os filhos, e o pai não termina até que eles terminem. É isso que evita aquele trabalho em background vazado que você tem com threads cruas. No Android, o viewModelScope é cancelado quando o ViewModel é limpo, e o lifecycleScope quando o lifecycle termina, então o trabalho para quando o resultado dele já não pode mais ser usado.
4
Explique os Dispatchers e qual você usa para quê.
Resposta
Dispatchers.Main roda na thread de UI e é onde você mexe em views. Dispatchers.IO é um pool grande e elástico para trabalho bloqueante — disco, rede, banco de dados — porque essas threads passam a maior parte do tempo esperando. Dispatchers.Default é dimensionado pelo número de CPUs e serve para computação de verdade, como parsing ou ordenação. Colocar uma chamada bloqueante no Default esgota o pool, que é o erro de dispatcher mais comum.
5
Qual é a diferença entre Flow, StateFlow e LiveData?
Resposta
Flow é um stream assíncrono frio: ele começa a produzir quando é coletado, e cada coletor tem sua própria execução. StateFlow é quente e sempre guarda exatamente um valor atual, o que o torna natural para estado de UI — novos coletores recebem imediatamente o estado mais recente. LiveData é o holder mais antigo, ciente do lifecycle; StateFlow junto com repeatOnLifecycle cobre a mesma necessidade usando o resto do toolkit de coroutines, então código novo geralmente prefere essa combinação.
6
Por que uma mudança de configuração recria a Activity, e como você sobrevive a isso?
Resposta
Rotação, modo escuro, mudança de idioma e de tamanho de janela destroem e recriam a Activity para que ela possa carregar recursos para a nova configuração. Estado que precisa sobreviver deve ficar em um ViewModel, que sobrevive à recriação. Estado que também precisa sobreviver à morte do processo vai no SavedStateHandle, porque o sistema pode matar seu processo em background e restaurar a task depois — testar esse caminho com "Não manter atividades" é o que separa um app robusto de um que perde o que o usuário digitou.
7
O que dispara a recomposição no Jetpack Compose, e o que a deixa lenta?
Resposta
O Compose roda de novo um composable quando um State que ele lê muda, e pula composables cujos parâmetros são iguais e estáveis. A lentidão vem de parâmetros instáveis — uma List simples ou uma lambda capturando um valor que muda — que derrotam esse pulo e fazem subárvores grandes rodarem de novo. Os consertos são elevar o estado para o dono comum mais próximo, usar coleções imutáveis ou anotações @Immutable, e adiar leituras com lambdas para que só a fase de layout ou draw rode de novo.
8
Por que usar injeção de dependência com Hilt em vez de construir objetos diretamente?
Resposta
DI tira da classe a necessidade de saber como construir suas dependências, o que a torna testável e mantém a fiação em um lugar só em vez de singletons espalhados. O Hilt gera o grafo do Dagger e amarra o tempo de vida dos componentes ao do Android — um @Singleton vive com a aplicação, um binding @ViewModelScoped vive com o ViewModel. O ganho prático é trocar um repositório real por um fake nos testes sem tocar no código de produção.
9
Como você estrutura uma tela em termos de clean architecture?
Resposta
A camada de UI tem Compose ou Views mais um ViewModel que expõe estado imutável e recebe eventos. A camada de domínio tem use cases e regras de negócio puras, sem imports do Android, o que é o que a torna testável com JUnit puro. A camada de dados tem repositórios que decidem entre Room e Retrofit e mapeiam DTOs para modelos de domínio. A regra que importa é a direção da dependência: o domínio não sabe nada sobre dados ou UI.
10
O que causa um ANR, e como encontrar um depois do release?
Resposta
Um ANR dispara quando a thread principal fica bloqueada por cerca de cinco segundos em uma entrada do usuário, com limites mais curtos para broadcasts e services. As causas típicas são chamadas de banco de dados ou rede na thread principal, um parse de JSON síncrono enorme, ou disputa de lock com uma thread em background. Depois do release, os relatórios de ANR do Play Console junto com a stack da thread principal capturada apontam para o frame que travou; adicionar StrictMode em builds de debug pega a maioria disso muito antes de ir para produção.
11
Como você evita vazamentos de memória no Android?
Resposta
A causa recorrente é algo de vida longa segurando uma Activity ou View — uma referência estática, um listener que você nunca removeu, uma classe interna com uma referência implícita para a externa, ou uma coroutine no GlobalScope. As defesas são limitar o trabalho a um scope ciente do lifecycle, limpar referências de binding no onDestroyView dos fragments, e usar o Context da aplicação onde não é necessário um Context de Activity. O LeakCanary em builds de debug pega o resto, mostrando o caminho que está retendo o objeto.
12
O que são as scope functions, e quando cada uma fica mais legível?
Resposta
let recebe o receiver como argumento e retorna o resultado da lambda, então serve bem para checagem de null e transformação. run faz o mesmo, mas com this, útil para um bloco de chamadas que retorna um valor. apply retorna o receiver e serve para configurar um objeto. also também retorna o receiver, mas o passa como it, então serve para efeitos colaterais como logging. with não é uma extension e só agrupa chamadas em um objeto. Escolher pelo valor de retorno e pela forma do receiver é o que mantém isso legível em vez de apenas esperto.