O que é Callback em Kotlin?

Um callback é uma função passada como argumento para outra função, que sera executada em um momento posterior – geralmente quando uma operação assíncrona termina ou quando um evento específico ocorre. Em Kotlin, callbacks são implementados de forma elegante usando lambdas, funções de ordem superior e interfaces funcionais.

O conceito e antigo e existe em praticamente todas as linguagens, mas em Kotlin ele ganha uma sintaxe limpa e concisa que torna o código muito mais legivel do que em linguagens como Java.

Callback básico com lambda

A forma mais simples de callback em Kotlin e passar uma lambda como parametro:

fun buscarDados(onSucesso: (String) -> Unit, onErro: (Exception) -> Unit) {
    try {
        // Simula busca de dados
        val resultado = "Dados carregados"
        onSucesso(resultado)
    } catch (e: Exception) {
        onErro(e)
    }
}

fun main() {
    buscarDados(
        onSucesso = { dados -> println("Sucesso: $dados") },
        onErro = { erro -> println("Erro: ${erro.message}") }
    )
}

Aqui, onSucesso e onErro são callbacks. A função buscarDados recebe as duas lambdas e chama a apropriada dependendo do resultado da operação.

Callback com interface funcional

Em cenários de interoperabilidade com Java ou quando você quer um contrato mais explicito, você pode usar interfaces funcionais:

fun interface OnResultadoListener {
    fun onResultado(dados: String)
}

class Repositorio {
    fun carregar(listener: OnResultadoListener) {
        // Simula processamento
        val resultado = "Dados do servidor"
        listener.onResultado(resultado)
    }
}

fun main() {
    val repo = Repositorio()

    // Usando SAM conversion
    repo.carregar { dados ->
        println("Recebido: $dados")
    }

    // Ou de forma explicita
    repo.carregar(object : OnResultadoListener {
        override fun onResultado(dados: String) {
            println("Recebido: $dados")
        }
    })
}

A palavra-chave fun interface permite que o Kotlin converta automaticamente uma lambda em uma implementação da interface (SAM conversion), mantendo o código conciso.

Callback para eventos de UI

Um uso clássico de callbacks e em componentes de interface grafica, como botoes:

class Botao {
    private var onClickListener: (() -> Unit)? = null

    fun setOnClickListener(listener: () -> Unit) {
        onClickListener = listener
    }

    fun clicar() {
        onClickListener?.invoke()
    }
}

fun main() {
    val botao = Botao()
    botao.setOnClickListener {
        println("Botao foi clicado!")
    }
    botao.clicar() // Botao foi clicado!
}

Esse padrão e fundamental no desenvolvimento Android, onde callbacks conectam acoes do usuário a lógica de negócio.

Callback hell e como evitar

O problema clássico com callbacks e o callback hell – aninhamento excessivo que torna o código ilegivel:

// Callback hell -- evite isso
fun carregarTudo() {
    buscarUsuario { usuario ->
        buscarPedidos(usuario.id) { pedidos ->
            buscarDetalhes(pedidos.first().id) { detalhes ->
                buscarEndereco(detalhes.enderecoId) { endereco ->
                    println("Endereco: $endereco")
                }
            }
        }
    }
}

Em Kotlin, a solução moderna para esse problema são as coroutines, que permitem escrever código assíncrono de forma sequencial:

// Com coroutines -- muito mais limpo
suspend fun carregarTudo() {
    val usuario = buscarUsuario()
    val pedidos = buscarPedidos(usuario.id)
    val detalhes = buscarDetalhes(pedidos.first().id)
    val endereco = buscarEndereco(detalhes.enderecoId)
    println("Endereco: $endereco")
}

Convertendo callbacks para coroutines

Quando você trabalha com APIs baseadas em callbacks (especialmente bibliotecas Java), pode converte-las para suspending functions usando suspendCoroutine ou suspendCancellableCoroutine:

import kotlinx.coroutines.*
import kotlin.coroutines.*

// API legada baseada em callback
fun buscarDadosLegado(callback: (Result<String>) -> Unit) {
    // Simula operacao assíncrona
    callback(Result.success("Dados legados"))
}

// Wrapper com coroutines
suspend fun buscarDadosSuspend(): String = suspendCancellableCoroutine { continuation ->
    buscarDadosLegado { resultado ->
        resultado
            .onSuccess { continuation.resume(it) }
            .onFailure { continuation.resumeWithException(it) }
    }
}

fun main() = runBlocking {
    val dados = buscarDadosSuspend()
    println(dados)
}

Essa técnica e extremamente útil ao migrar código legado para coroutines de forma incremental.

Callback com tipo generico

Você pode criar callbacks reutilizaveis usando generics:

typealias Callback<T> = (Result<T>) -> Unit

fun <T> executarAsync(operacao: () -> T, callback: Callback<T>) {
    try {
        val resultado = operacao()
        callback(Result.success(resultado))
    } catch (e: Exception) {
        callback(Result.failure(e))
    }
}

fun main() {
    executarAsync(
        operacao = { 42 * 2 },
        callback = { resultado ->
            resultado
                .onSuccess { println("Resultado: $it") }
                .onFailure { println("Erro: ${it.message}") }
        }
    )
}

O typealias torna a assinatura mais legivel, e o uso de Result padroniza o tratamento de sucesso e erro.

Quando usar callbacks

Callbacks ainda são úteis em vários cenários:

  • Interoperabilidade com Java: muitas bibliotecas Java usam o padrão listener/callback, e você precisa se adaptar.
  • Eventos de UI: cliques, gestos e outros eventos de interface são naturalmente modelados como callbacks.
  • APIs simples: quando a operação e direta e não há aninhamento, um callback e perfeitamente adequado.
  • Bibliotecas e frameworks: se você esta criando uma biblioteca que precisa ser usada por projetos que não usam coroutines.

Porem, para lógica assíncrona complexa com múltiplas etapas, prefira coroutines. Elas eliminam o aninhamento é fácilitam o tratamento de erros com try-catch normal.

Casos de Uso no Mundo Real

  1. Listeners de eventos em Android: toda interação do usuário com a interface – cliques em botoes, mudancas em campos de texto, gestos de swipe – e modelada como callback. O setOnClickListener do Android e o exemplo mais clássico, onde uma lambda e registrada para ser executada quando o usuário interage com o componente.

  2. Respostas de APIs de rede: bibliotecas como Retrofit e OkHttp usam callbacks para notificar quando uma requisicao HTTP foi concluida. O callback recebe a resposta do servidor ou um objeto de erro, permitindo que o código que fez a requisicao processe o resultado sem bloquear a thread principal.

  3. Observadores de ciclo de vida: no Android, callbacks como onStart(), onResume(), onPause() e onDestroy() notificam o componente sobre mudancas no ciclo de vida. Frameworks como o Lifecycle do AndroidX usam esse padrão para que bibliotecas reajam automaticamente a mudancas de estado da Activity ou Fragment.

  4. Processamento de arquivos e streams: operações de leitura/escrita em arquivos grandes frequentemente usam callbacks para reportar progresso. Em vez de bloquear a thread até a operação terminar, o sistema notifica o chamador a cada bloco processado, permitindo exibir barras de progresso ou cancelar a operação.

Boas Praticas

  • Prefira lambdas com parametros nomeados na assinatura da função (onSucesso, onErro) em vez de um único callback generico. Nomes explicitos tornam a intencao clara e facilitam a leitura do código no ponto de chamada.
  • Garanta que o callback seja chamado em exatamente um caminho de execução. Esquecer de chamar o callback em algum branch condicional causa bugs silenciosos, e chama-lo mais de uma vez pode causar comportamento inesperado.
  • Ao trabalhar com callbacks em Android, sempre considere o ciclo de vida do componente. Cancele ou remova callbacks quando a Activity ou Fragment for destruído para evitar memory leaks e crashes por referência a contextos invalidos.
  • Para APIs novas, prefira coroutines e suspend fun em vez de callbacks. Use suspendCancellableCoroutine para adaptar APIs legadas baseadas em callback para o mundo das coroutines, facilitando composicao e tratamento de erros.
  • Utilize typealias para dar nomes significativos a tipos de callback complexos, como typealias OnResultado<T> = (Result<T>) -> Unit. Isso melhora a legibilidade das assinaturas de funções sem adicionar overhead.

Perguntas Frequentes

P: Quando devo usar callbacks em vez de coroutines em Kotlin? R: Use callbacks quando estiver trabalhando com interoperabilidade Java (bibliotecas que já usam o padrão listener), eventos de UI simples (cliques, gestos) ou quando estiver criando bibliotecas que precisam ser acessiveis a projetos que não usam coroutines. Para lógica assíncrona com múltiplas etapas, prefira coroutines.

P: Como evitar memory leaks com callbacks em Android? R: Registre callbacks em onStart() ou onResume() e remova-os em onStop() ou onPause(). Evite usar lambdas que capturam referência a Activities ou Views. Se necessário, use WeakReference ou migre para LiveData/Flow que respeitam o ciclo de vida automaticamente.

P: Qual a diferenca entre um callback e um listener em Kotlin? R: Na prática, sao o mesmo conceito. “Listener” e o termo mais comum no ecossistema Android e Java, geralmente implementado como uma interface com um ou mais métodos. “Callback” e o termo generico da programacao para qualquer função passada como argumento para ser chamada posteriormente. Em Kotlin, ambos podem ser implementados como lambdas.

P: E possível converter qualquer API baseada em callback para coroutines? R: Sim, usando suspendCancellableCoroutine. Voce encapsula a chamada da API legada dentro dessa função, e no callback de sucesso chama continuation.resume(valor), e no callback de erro chama continuation.resumeWithException(exceção). Isso transforma a API em uma suspend fun que pode ser chamada de forma sequencial.

Erros comuns

  1. Callback hell: aninhar callbacks excessivamente. Use coroutines ou quebre a lógica em funções menores.

  2. Esquecer de chamar o callback: se a função tem caminhos de execução diferentes (sucesso, erro, timeout), certifique-se de que o callback e chamado em todos eles.

  3. Chamar o callback mais de uma vez: a maioria dos contratos espera que o callback seja chamado exatamente uma vez. Chamar múltiplas vezes pode causar comportamento inesperado.

  4. Nao tratar erros: passar apenas um callback de sucesso e ignorar falhas. Sempre forneca um mecanismo para tratar erros.

  5. Reter referências de callback: em Android, manter uma referência a um callback que contém uma referência a uma Activity pode causar memory leaks. Use WeakReference ou cancele o callback quando o componente for destruído.

Termos relacionados

  • Lambda: a forma mais comum de expressar callbacks em Kotlin, usando a sintaxe { parametros -> corpo }.
  • Higher-Order Function: funções que recebem ou retornam outras funções, o mecanismo que permite callbacks em Kotlin.
  • Coroutine: alternativa moderna aos callbacks para programação assíncrona, eliminando o callback hell.
  • suspend: modificador que transforma uma função em uma função suspensa, substituindo callbacks por retorno direto.
  • Flow: stream reativo que substitui callbacks repetitivos por um fluxo de dados continuo.
  • SAM Conversion: mecanismo que converte lambdas em implementacoes de interfaces funcionais Java.

Callbacks são um dos padrões mais fundamentais da programação. Em Kotlin, a combinacao de lambdas, funções de ordem superior e interfaces funcionais torna sua implementação elegante e expressiva, enquanto coroutines oferecem uma evolução natural para cenários mais complexos.