O que e Callback em Kotlin?

Um callback e uma funcao passada como argumento para outra funcao, que sera executada em um momento posterior – geralmente quando uma operacao assincrona termina ou quando um evento especifico ocorre. Em Kotlin, callbacks sao implementados de forma elegante usando lambdas, funcoes 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 codigo muito mais legivel do que em linguagens como Java.

Callback basico 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 sao callbacks. A funcao buscarDados recebe as duas lambdas e chama a apropriada dependendo do resultado da operacao.

Callback com interface funcional

Em cenarios de interoperabilidade com Java ou quando voce quer um contrato mais explicito, voce 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 implementacao da interface (SAM conversion), mantendo o codigo conciso.

Callback para eventos de UI

Um uso classico 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 padrao e fundamental no desenvolvimento Android, onde callbacks conectam acoes do usuario a logica de negocio.

Callback hell e como evitar

O problema classico com callbacks e o callback hell – aninhamento excessivo que torna o codigo 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 solucao moderna para esse problema sao as coroutines, que permitem escrever codigo assincrono 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 voce 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 assincrona
    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 tecnica e extremamente util ao migrar codigo legado para coroutines de forma incremental.

Callback com tipo generico

Voce 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 sao uteis em varios cenarios:

  • Interoperabilidade com Java: muitas bibliotecas Java usam o padrao listener/callback, e voce precisa se adaptar.
  • Eventos de UI: cliques, gestos e outros eventos de interface sao naturalmente modelados como callbacks.
  • APIs simples: quando a operacao e direta e nao ha aninhamento, um callback e perfeitamente adequado.
  • Bibliotecas e frameworks: se voce esta criando uma biblioteca que precisa ser usada por projetos que nao usam coroutines.

Porem, para logica assincrona complexa com multiplas etapas, prefira coroutines. Elas eliminam o aninhamento e facilitam o tratamento de erros com try-catch normal.

Erros comuns

  1. Callback hell: aninhar callbacks excessivamente. Use coroutines ou quebre a logica em funcoes menores.

  2. Esquecer de chamar o callback: se a funcao tem caminhos de execucao 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 multiplas 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 referencias de callback: em Android, manter uma referencia a um callback que contem uma referencia a uma Activity pode causar memory leaks. Use WeakReference ou cancele o callback quando o componente for destruido.

Termos relacionados

  • Lambda: a forma mais comum de expressar callbacks em Kotlin, usando a sintaxe { parametros -> corpo }.
  • Higher-Order Function: funcoes que recebem ou retornam outras funcoes, o mecanismo que permite callbacks em Kotlin.
  • Coroutine: alternativa moderna aos callbacks para programacao assincrona, eliminando o callback hell.
  • suspend: modificador que transforma uma funcao em uma funcao 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 sao um dos padroes mais fundamentais da programacao. Em Kotlin, a combinacao de lambdas, funcoes de ordem superior e interfaces funcionais torna sua implementacao elegante e expressiva, enquanto coroutines oferecem uma evolucao natural para cenarios mais complexos.