Se você já trabalhou com extension functions em Kotlin, sabe como elas tornam o código mais expressivo. Mas e quando uma função precisa de múltiplos contextos ao mesmo tempo? É aí que entram os Context Receivers e sua evolução, os Context Parameters. Neste guia, vamos explorar como essas funcionalidades transformam a forma de escrever código Kotlin.

O que são Context Receivers?

Context Receivers foram introduzidos experimentalmente no Kotlin 1.6.20 como uma forma de declarar que uma função precisa de um ou mais contextos implícitos para ser chamada. Diferente de extension functions, que possuem apenas um receiver, os context receivers permitem múltiplos.

A ideia central é simples: em vez de passar dependências explicitamente como parâmetros, você declara que a função requer um determinado contexto para funcionar.

Sintaxe básica

context(LoggerContext)
fun processarPedido(pedido: Pedido) {
    log("Processando pedido ${pedido.id}")
    // lógica de processamento
}

interface LoggerContext {
    fun log(mensagem: String)
}

Para chamar essa função, o chamador precisa estar em um escopo onde LoggerContext esteja disponível:

class ServicoDeVendas : LoggerContext {
    override fun log(mensagem: String) {
        println("[VENDAS] $mensagem")
    }

    fun vender(pedido: Pedido) {
        processarPedido(pedido) // funciona! LoggerContext está no escopo
    }
}

Context Receivers vs Extension Functions

Você pode estar pensando: “Isso parece uma extension function”. E de fato a motivação é semelhante, mas existem diferenças cruciais:

AspectoExtension FunctionContext Receiver
ReceiversApenas 1Múltiplos
Acesso ao thisSim, do receiverSim, de cada contexto
Uso primárioAdicionar métodos a tiposInjeção de contexto implícito
CombinaçãoDifícil combinar múltiplosNatural com múltiplos contextos

Exemplo prático com múltiplos contextos

interface TransactionContext {
    fun <T> executarTransacao(bloco: () -> T): T
}

interface NotificacaoContext {
    fun notificar(usuario: String, mensagem: String)
}

context(TransactionContext, NotificacaoContext, LoggerContext)
fun transferirSaldo(origem: Conta, destino: Conta, valor: Double) {
    log("Iniciando transferência de R$$valor")
    executarTransacao {
        origem.debitar(valor)
        destino.creditar(valor)
    }
    notificar(origem.titular, "Transferência de R$$valor realizada")
    log("Transferência concluída com sucesso")
}

Esse código é limpo, sem precisar passar cada dependência como parâmetro. Se você trabalha com Python e decorators, vai notar uma semelhança conceitual: ambos permitem injetar comportamento de forma declarativa, embora com mecanismos bem diferentes.

De Context Receivers para Context Parameters

A equipe do Kotlin percebeu que a implementação original de context receivers tinha limitações. Por isso, a partir do Kotlin 2.0+, a funcionalidade está evoluindo para Context Parameters — uma abordagem mais refinada e segura.

Principais diferenças

// Context Receivers (experimental, Kotlin 1.6.20+)
context(LoggerContext)
fun processarPedido(pedido: Pedido) { ... }

// Context Parameters (evolução futura)
fun processarPedido(context logger: LoggerContext, pedido: Pedido) { ... }

Com context parameters, o contexto é declarado de forma mais explícita na assinatura da função, tornando o código mais previsível e fácil de entender. As vantagens incluem:

  • Nomeação explícita: cada contexto tem um nome, evitando ambiguidades
  • Melhor inferência de tipos: o compilador K2 trabalha melhor com essa sintaxe
  • Compatibilidade com IDE: melhor autocomplete e navegação no IntelliJ IDEA

Casos de uso práticos

1. Injeção de dependências leve

Context receivers oferecem uma forma de injeção de dependências sem frameworks pesados como Koin ou Dagger:

interface RepositorioPedidos {
    fun buscarPorId(id: Long): Pedido?
    fun salvar(pedido: Pedido): Pedido
}

interface ServicoPagamento {
    fun processar(valor: Double, metodo: MetodoPagamento): ResultadoPagamento
}

context(RepositorioPedidos, ServicoPagamento, LoggerContext)
fun finalizarCompra(pedidoId: Long, metodo: MetodoPagamento): ResultadoCompra {
    val pedido = buscarPorId(pedidoId) ?: error("Pedido não encontrado")
    log("Processando pagamento para pedido $pedidoId")

    val resultado = processar(pedido.total, metodo)

    if (resultado.sucesso) {
        salvar(pedido.copy(status = Status.PAGO))
        log("Pedido $pedidoId pago com sucesso")
    }

    return ResultadoCompra(pedidoId, resultado)
}

2. Construção de DSLs mais expressivas

Se você já leu nosso guia sobre DSLs em Kotlin, sabe que lambdas com receiver são essenciais. Context receivers levam isso a outro nível:

interface HtmlContext {
    fun tag(nome: String, conteudo: String)
}

interface CssContext {
    fun estilo(seletor: String, propriedades: Map<String, String>)
}

context(HtmlContext, CssContext)
fun componenteCartao(titulo: String, descricao: String) {
    estilo(".cartao", mapOf(
        "border-radius" to "8px",
        "padding" to "16px",
        "box-shadow" to "0 2px 4px rgba(0,0,0,0.1)"
    ))
    tag("div", """
        <h3>$titulo</h3>
        <p>$descricao</p>
    """)
}

3. Logging e monitoramento com contexto

Uma aplicação prática e comum é adicionar contexto de observabilidade sem poluir assinaturas de funções:

interface TracingContext {
    val traceId: String
    fun span(nome: String, bloco: () -> Unit)
}

context(TracingContext, LoggerContext)
fun processarEvento(evento: Evento) {
    span("processar-evento") {
        log("[$traceId] Evento recebido: ${evento.tipo}")
        // processamento do evento
    }
}

Como habilitar Context Receivers hoje

Para experimentar context receivers no seu projeto, adicione ao build.gradle.kts:

tasks.withType<KotlinCompile> {
    compilerOptions {
        freeCompilerArgs.add("-Xcontext-receivers")
    }
}

Atenção: Context receivers ainda são experimentais. A API pode mudar em versões futuras do Kotlin. Para projetos em produção, avalie cuidadosamente e acompanhe as notas de release oficiais.

Relação com Coroutines e Scope Functions

Se você usa coroutines, já está familiarizado com o conceito de escopo contextual — CoroutineScope funciona de maneira análoga. E as scope functions (let, run, with, apply, also) são primas próximas, operando com receivers implícitos.

Context receivers estendem esse padrão, permitindo que qualquer função declare seus requisitos contextuais de forma tipada e segura.

Boas práticas

  1. Use com moderação: não transforme todo parâmetro em context receiver — use apenas para dependências transversais (logging, transações, configuração)
  2. Prefira interfaces: declare contextos como interfaces, não classes concretas, facilitando testes
  3. Documente os contextos: como a dependência é implícita, documentar qual contexto é necessário ajuda a manutenibilidade
  4. Acompanhe a evolução: migre para context parameters quando a funcionalidade estabilizar

Conclusão

Context Receivers e Context Parameters representam uma evolução natural do sistema de receivers do Kotlin. Eles resolvem o problema de injeção de múltiplos contextos de forma elegante, complementando as extension functions e as sealed classes no arsenal do desenvolvedor Kotlin.

Embora ainda experimentais, já mostram como o Kotlin continua inovando para tornar o código mais expressivo e seguro. Fique de olho nas próximas versões e comece a experimentar nos seus projetos pessoais.

Para se aprofundar na linguagem, confira nosso Guia Completo de Kotlin e o Glossário de Kotlin.

Se o conceito de injeção de contexto te interessou, vale explorar como outras linguagens resolvem problemas semelhantes: Go usa o pacote context para propagação de valores entre funções, enquanto Rust resolve isso com traits e generics no sistema de tipos.