Entrevista Kotlin: null safety, corrotinas e um caso de busca cancelável

Prepare a entrevista Kotlin com uma busca cancelável: trate nomes nulos, evite respostas antigas e teste corrotinas com tempo virtual e limites explícitos.

Author: PracHub

Published: 10/11/2026

Entrevista Kotlin: null safety, corrotinas e um caso de busca cancelável

October 11, 2026

Quick Overview

Resolva uma busca de produtos em Kotlin: políticas para null, cancelamento cooperativo, identidade de solicitações e estados de carregamento e erro. Compare respostas fora de ordem com nove verificações do núcleo JVM e uma adaptação ilustrativa para ViewModel.

Software EngineerFree

Uma entrevista Kotlin pode começar com uma busca simples e terminar em três perguntas diferentes: o que fazer com um nome nulo, como cancelar o trabalho anterior e por que uma resposta antiga ainda aparece na tela. Explique como ?. e launch afetam o estado observado pelo usuário e o caminho que o produz.

Neste exercício original, uma pessoa digita ca e logo depois cafe em uma busca de produtos. A primeira resposta chega por último. Vamos definir o contrato, corrigir o núcleo da busca e explicar como conectá-lo a um ViewModel. Você pode iniciar o treino com Design a Shopping Search Bar with Autocomplete, concentrando-se primeiro na correção, antes de discutir escala.

Limite da evidência: as regras de linguagem e corrotinas abaixo vêm da documentação oficial. O produto, os dados e os testes são exemplos nossos, não relatos de candidatos nem questões confirmadas de uma empresa. Verificamos o núcleo em Kotlin/JVM com compilador 2.2.0, JDK 21 e kotlinx-coroutines 1.10.2. A integração Android apresentada é uma orientação de código; não foi executada em dispositivo ou em uma interface real.

Busca de produtos com uma resposta antiga chegando depois da consulta mais recente

Defina o significado de null antes de removê-lo

A documentação oficial de null safety em Kotlin distingue tipos anuláveis e não anuláveis. Uma chamada segura pode propagar null; !! pode lançar uma exceção se o valor for nulo. Essas ferramentas não escolhem a regra de apresentação do produto por você.

No caso fictício, o texto de entrada pode ser nulo porque a ação de limpar a busca usa esse valor. A política escolhida é normalizar espaços e tratar entrada nula ou vazia como estado inicial, sem chamar o repositório. O nome retornado de um produto também pode ser nulo; para exibição, escolhemos “Produto sem nome”. O identificador, porém, já é uma string não anulável no contrato do repositório.

val query = raw?.trim().orEmpty()
val title = dto.name?.trim()
    ?.takeIf { it.isNotEmpty() }
    ?: "Produto sem nome"

Explique por que as duas políticas são diferentes. Uma busca vazia não deve ser transformada no texto literal "null". Um produto sem nome não deve provocar crash nem receber um nome inventado. Em outro domínio, rejeitar o registro poderia ser a decisão correta; aqui queremos mostrar o item com um rótulo explícito de ausência.

O DTO não é uma prova de que qualquer JSON recebido será válido. A desserialização e a validação de identificadores pertencem à fronteira de dados e não foram implementadas neste exercício. Também não discutimos unicidade de IDs ou moeda. Separe as garantias do tipo das hipóteses sobre o sistema externo.

Leia o traço que permite a resposta antiga vencer

Considere a implementação defeituosa que inicia uma corrotina para cada mudança de texto e publica qualquer resultado que retornar. Ela não cancela a busca anterior e não verifica a identidade da solicitação. O erro pode acontecer mesmo que todos os nomes sejam não nulos e nenhuma exceção seja lançada.

Instante do exercícioEventoEstado que deveria permanecer relevante
0 msA busca ca começa e levará 300 ms.A intenção atual é ca.
0 ms, depoisA busca cafe começa e levará 50 ms.A intenção atual passa a ser cafe.
50 mscafe retorna primeiro.Mostrar produtos de cafe.
300 msca termina depois.Não substituir o resultado mais recente.

Os tempos são controlados pelo teste, não medições de rede. O defeito é publicar pela ordem de conclusão quando o contrato exige respeitar a intenção mais recente. “A API foi lenta” descreve uma condição que expôs o problema; não justifica a interface mostrar uma busca que a pessoa já substituiu.

Na entrevista, diga: “Vou associar cada intenção a uma geração, cancelar o job anterior e conferir se a corrotina ainda está ativa antes de publicar. Todas as mudanças deste modelo ocorrerão no mesmo dispatcher.” Declare o confinamento: um contador simples não torna chamadas simultâneas de várias threads seguras.

Cancelamento e identidade têm funções diferentes

A referência oficial de ensureActive descreve cancelamento cooperativo. Job.cancel() sinaliza a interrupção; código que não coopera pode continuar trabalhando. O ponto de verificação antes de publicar impede que uma corrotina cancelada prossiga com essa alteração de estado.

A geração verifica a identidade da intenção à qual a resposta pertence. No desenho mostrado, cancelar o job e verificar sua atividade já cobre o retorno obsoleto sob as precondições descritas. A geração torna essa identidade explícita e oferece uma defesa adicional; não é uma desculpa para ignorar cancelamento ou acesso concorrente.

Comparar somente o texto não expressa a mesma identidade. A pessoa pode pesquisar ca, depois cafe, depois ca novamente. O primeiro e o terceiro pedidos têm o mesmo texto, mas são intenções diferentes. Um contador crescente permite distinguir esses pedidos sem presumir que seus resultados sejam intercambiáveis.

A normalização também precisa invalidar trabalho pendente ao limpar o campo. Por isso incrementamos a geração e cancelamos o job anterior antes do retorno para consulta vazia. Se a função retornar primeiro, uma resposta pendente pode repovoar a tela depois de a pessoa ter apagado o texto.

Um núcleo de busca pequeno e testável

Usamos ProductDto(id: String, name: String?), ProductUi(id: String, title: String) e um estado com query, loading, items e error. O repositório expõe suspend fun search(query: String): List<ProductDto>. Este trecho é a parte central do modelo; os tipos e imports devem existir no módulo.

class SearchModel(
    private val scope: CoroutineScope,
    private val repo: ProductRepository
) {
    private var generation = 0L
    private var job: Job? = null
    private val mutable = MutableStateFlow(SearchState())
    val state: StateFlow<SearchState> = mutable.asStateFlow()

    fun search(raw: String?) {
        val query = raw?.trim().orEmpty()
        val id = ++generation
        job?.cancel()
        if (query.isEmpty()) {
            mutable.value = SearchState()
            return
        }
        mutable.value = SearchState(query = query, loading = true)
        job = scope.launch {
            try {
                val rows = repo.search(query)
                currentCoroutineContext().ensureActive()
                if (id == generation) {
                    mutable.value = SearchState(
                        query = query,
                        items = rows.map {
                            ProductUi(it.id, it.name?.trim()
                                ?.takeIf { name -> name.isNotEmpty() }
                                ?: "Produto sem nome")
                        }
                    )
                }
            } catch (e: CancellationException) {
                throw e
            } catch (e: IOException) {
                currentCoroutineContext().ensureActive()
                if (id == generation) {
                    mutable.value = SearchState(
                        query = query,
                        error = "Não foi possível buscar"
                    )
                }
            }
        }
    }
}

Há uma decisão de produto visível: uma nova busca limpa a lista anterior e entra em carregamento. Manter resultados antigos durante o carregamento seria outra política, que exigiria preservar os itens e indicar a qual consulta pertencem. Não altere essa experiência por acidente ao corrigir uma condição de concorrência.

O modelo publica snapshots de estado e expõe StateFlow somente para leitura. Isso não elimina a precondição de confinamento para generation e job. No Android, o ponto de entrada seria chamado na thread principal; no teste, usamos um scheduler único. Não apresentamos a classe como segura para invocações arbitrárias de várias threads.

Não transforme cancelamento em mensagem de falha

A documentação oficial de tratamento de exceções em corrotinas explica o papel de CancellationException no cancelamento. Neste exercício, capturamos esse sinal apenas para relançá-lo. Uma busca substituída não deve produzir um aviso de erro sobre a busca nova.

A falha recuperável definida pelo contrato é IOException. O exemplo mostra uma mensagem de interface sem expor o detalhe interno da exceção. Erros de programação não são convertidos indiscriminadamente em “sem resultados”. Se o repositório usar outro tipo para falhas de domínio, o contrato e os testes precisam ser ajustados.

Mesmo no catch de IOException, conferimos atividade e geração antes de publicar. Um repositório mal adaptado pode terminar uma operação antiga e reportar erro depois de ter sido cancelado. Esse erro não deve substituir o sucesso da consulta atual. O teste inclui explicitamente esse caso, em vez de verificar apenas respostas bem-sucedidas.

Evite um finally que sempre coloque loading = false no estado compartilhado. O término de uma busca antiga pode ocorrer enquanto a busca atual ainda carrega. No modelo, cada publicação autorizada constrói o estado correspondente à intenção atual. Encerrar um job antigo não lhe dá permissão para editar o estado novo.

Consulta atual preservada enquanto resultado e erro antigos são descartados

Como conectar o núcleo ao ViewModel

A documentação Android sobre corrotinas com componentes de ciclo de vida informa que corrotinas de viewModelScope são canceladas quando o ViewModel é limpo. Uma adaptação pequena pode possuir o modelo e delegar a entrada, evitando um scope global criado somente para escapar do ciclo de vida.

class ProductSearchViewModel(repo: ProductRepository) : ViewModel() {
    private val model = SearchModel(viewModelScope, repo)
    val state = model.state
    fun search(text: String?) = model.search(text)
}

Esse adaptador requer as dependências e imports de AndroidX apropriados. Ele é ilustrativo e não foi compilado ou executado no ambiente Android desta verificação. Os testes executados cobrem a classe Kotlin que recebe o scope, incluindo cancelamento pelo proprietário, não navegação, rotação ou coleta de estado na interface.

Também não presuma que limpar o ViewModel significa desfazer uma operação no servidor. A busca do exemplo não escreve dados. Se o repositório usar uma biblioteca HTTP, é preciso verificar como ela adapta cancelamento à chamada real. Descartar um resultado obsoleto corrige a publicação local; interromper bytes de rede e trabalho remoto são garantias diferentes.

Teste a ordem inversa sem dormir a thread

A biblioteca oficial kotlinx-coroutines-test oferece ferramentas para controlar o scheduler em testes. Nossa suíte usa runTest, runCurrent e avanço de tempo virtual. Assim, o teste decide quando uma busca começa e quando seus resultados ficam prontos, sem depender de atrasos reais do sistema operacional.

O repositório falso faz a primeira busca aguardar 300 ms virtuais e a segunda 50. Para testar uma dependência que não coopera bem, uma variante usa NonCancellable deliberadamente no fake. Isso é uma técnica adversarial do teste, não uma recomendação para blindar a busca real contra cancelamento.

Verifique o estado aos 50 ms: consulta cafe, carregamento encerrado e item de cafe. Depois avance até 300 ms e confira que o item continua sendo o mais recente, embora o fake registre a conclusão de ca. O registro mostra que a busca antiga concluiu seu trabalho, mas não substituiu o estado atual.

Executamos nove verificações: entrada nula ou vazia; normalização e nome ausente; resultado novo primeiro; resultado antigo descartado; falha atual visível; falha antiga ignorada; limpeza com trabalho pendente; sequência ca → cafe → ca; e cancelamento pelo scope proprietário. Uma variante sem cancelamento anterior nem guarda de geração falhou na verificação do resultado antigo.

Os nove checks sustentam os caminhos executados do núcleo em JVM. Não demonstram toda a integração com Android, segurança entre threads ou cancelamento de uma rede real. Se o entrevistador pedir essas garantias, explique o próximo teste necessário: integração com a biblioteca HTTP, comportamento do ciclo de vida ou chamadas concorrentes conforme o contrato.

Reduzir chamadas por um intervalo de espera antes da busca é outra decisão, que não substitui a correção de identidade. Uma consulta pode já estar em andamento quando o texto muda, mesmo com debounce. Da mesma forma, um cache não garante que um resultado pertença à intenção atual. Se você acrescentar qualquer uma dessas otimizações, preserve os testes de resposta tardia e de limpeza do campo; depois teste separadamente a nova regra de frequência ou reutilização. Não confunda menos requisições com ausência de resultados obsoletos.

Use perguntas públicas para ampliar o treino

Esta seleção editorial do PracHub permite variar o exercício. Os enunciados não são todos específicos de Kotlin; use-os para discutir comportamento de busca, estados e limites. Eles não constituem uma lista de perguntas garantidas para uma empresa ou nível de senioridade.

Pergunta completaO que explicar em Kotlin
Design a Shopping Search Bar with AutocompleteQual resultado pode atualizar a consulta atual.
Design a Prefix-Suggestion Search InterfaceEstados vazio, carregando, sucesso e erro.
Build a Paginated Provider Search Backend and React Filter UIPreserve a identidade da consulta ao adicionar filtros ou páginas.
Fix the Broken Search Filter in a Book Catalog APISepare defeito de contrato no servidor de resultado obsoleto na interface.
Review concurrent code qualityDeclare o confinamento e identifique cada publicação compartilhada.

Para ensaiar, explique primeiro o traço ca → cafe sem código. Depois mostre o ponto de verificação antes de publicar e a política para nomes ausentes. Acrescente a sequência com o mesmo texto repetido e diga por que igualdade de strings não identifica o pedido.

Continue com Design a Prefix-Suggestion Search Interface. Defenda a política de carregamento, mostre um teste de resposta fora de ordem e indique o próximo teste de integração.

Sources and Further Reading


Comments (0)