O que é Context Receiver em Kotlin?
Context Receivers são um recurso do Kotlin que permite declarar que uma função ou propriedade requer um ou mais contextos implícitos para ser chamada. Diferente de extension functions, que tem um único receiver, context receivers permitem que uma função dependa de múltiplos objetos de contexto sem precisar recebe-los como parametros explicitos.
Esse recurso foi introduzido como experimental a partir do Kotlin 1.6.20 e continua evoluindo. Ele resolve um problema antigo: como expressar que uma função precisa de múltiplos “ambientes” para funcionar, sem poluir a assinatura com muitos parametros.
O problema que Context Receivers resolvem
Imagine que você tem uma função que precisa de acesso a um logger é a uma transacao de banco de dados. Sem context receivers, você teria estas opções:
// Opcao 1: Parametros explicitos (verboso)
fun salvarUsuario(usuario: Usuario, logger: Logger, transacao: Transacao) {
logger.info("Salvando usuario...")
transacao.executar { /* salvar */ }
}
// Opcao 2: Extension function (limitado a um receiver)
fun Logger.salvarUsuario(usuario: Usuario, transacao: Transacao) {
info("Salvando usuario...")
transacao.executar { /* salvar */ }
}
Nenhuma dessas opções expressa claramente que a função depende de ambos os contextos. Com context receivers, a intencao fica explicita:
context(Logger, Transacao)
fun salvarUsuario(usuario: Usuario) {
info("Salvando usuario...")
executar { /* salvar */ }
}
Sintaxe e declaracao
A sintaxe usa a palavra-chave context seguida dos tipos de contexto entre parenteses:
context(LoggerContext)
fun registrarEvento(mensagem: String) {
log("Evento: $mensagem")
}
context(LoggerContext, MetricasContext)
fun registrarComMetricas(mensagem: String) {
log("Evento: $mensagem")
incrementarContador("eventos.registrados")
}
Para chamar essas funções, você precisa estar em um escopo onde os contextos estejam disponiveis:
interface LoggerContext {
fun log(mensagem: String)
}
interface MetricasContext {
fun incrementarContador(nome: String)
}
fun main() {
val logger = object : LoggerContext {
override fun log(mensagem: String) = println(mensagem)
}
val métricas = object : MetricasContext {
override fun incrementarContador(nome: String) = println("$nome +1")
}
with(logger) {
with(métricas) {
registrarComMetricas("Usuario criado")
}
}
}
Exemplo prático: DSL com múltiplos contextos
Context receivers brilham na criação de DSLs onde diferentes escopos precisam estar disponiveis:
interface HtmlContext {
fun tag(nome: String, conteudo: String)
}
interface CssContext {
fun estilo(seletor: String, propriedade: String, valor: String)
}
context(HtmlContext, CssContext)
fun componenteEstilizado(nome: String, texto: String) {
estilo(".$nome", "color", "blue")
estilo(".$nome", "font-weight", "bold")
tag("div class='$nome'", texto)
}
Isso permite que a função componenteEstilizado acesse tanto a API de HTML quanto a de CSS sem receber nenhum parametro adicional.
Context Receivers em classes
Você também pode usar context receivers em classes e propriedades:
interface Configuração {
val maxTentativas: Int
val timeout: Long
}
context(Configuração)
class ServicoDeRetentativa {
fun <T> executar(bloco: () -> T): T {
var tentativa = 0
while (true) {
try {
return bloco()
} catch (e: Exception) {
tentativa++
if (tentativa >= maxTentativas) throw e
Thread.sleep(timeout)
}
}
}
}
context(Configuração)
val tempoTotalMaximo: Long
get() = maxTentativas * timeout
Context Receivers vs Extension Functions
A diferenca fundamental e a quantidade de receivers:
// Extension function: um unico receiver
fun String.formatarComoTitulo(): String {
return this.split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
}
// Context receiver + extension: múltiplos contextos
context(Locale)
fun String.formatarLocalizado(): String {
return this.uppercase(this@Locale)
}
Extension functions são ideais quando a função e logicamente um “método” daquele tipo. Context receivers são para quando a função precisa de um ambiente/contexto para funcionar, mas não e um método do contexto.
Habilitando context receivers
Como o recurso ainda e experimental, você precisa habilita-lo no build.gradle.kts:
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xcontext-receivers"
}
}
Sem essa flag, o compilador não reconhecera a sintaxe context(...).
Quando usar Context Receivers
- DSLs complexas: quando o DSL tem múltiplas camadas de contexto que precisam estar acessiveis simultaneamente.
- Injeção de dependências implicita: quando funções dependem de serviços ou configurações que deveriam estar “no ambiente” sem serem passados explicitamente.
- separação de concerns: quando você quer expressar que uma função depende de capacidades específicas (logging, métricas, transacao) sem acoplar a implementacoes concretas.
- Reducao de parametros: quando muitas funções no mesmo modulo compartilham os mesmos parametros de contexto.
Evite usar quando a função tem apenas um contexto (use extension function) ou quando os parametros são dados, não ambientes (use parametros normais).
Casos de Uso no Mundo Real
Camadas de servico com logging e transacoes: em aplicações backend, funções de servico frequentemente precisam de acesso a um logger, a uma transacao de banco de dados e a um contexto de autenticação. Com context receivers, essas dependências ficam implicitas na assinatura da função, eliminando a repeticao de parametros em dezenas de métodos sem recorrer a injecao global.
DSLs para geracao de documentos e relatorios: ao construir DSLs que geram HTML, PDF ou relatorios estruturados, diferentes partes do documento precisam de acesso simultaneo a contextos de formatacao, estilo e dados. Context receivers permitem que funções como
adicionarTabelaouadicionarGraficoacessem tanto o contexto de layout quanto o de dados sem parametros extras.Frameworks de validacao com escopo: bibliotecas de validacao podem definir funções de regra que exigem um contexto de validacao. Regras como
campoObrigatorioouvalorMinimooperam dentro de umValidacaoContextque acumula erros, enquanto também acessam umLocalizacaoContextpara traduzir mensagens. Context receivers tornam essa composicao de escopos natural.APIs de teste com múltiplos ambientes: em frameworks de teste, funções auxiliares frequentemente precisam de acesso a um banco de dados de teste, um servidor mock e um contexto de assertivas. Context receivers permitem que funções como
inserirDadosDeTesteouverificarRespostadeclarem explicitamente de quais ambientes dependem, sem criar classes gigantes com todas as dependências.
Boas Praticas
- Use context receivers para dependências de ambiente (logging, transacoes, configuração), não para dados de negócio. Dados que variam por chamada devem continuar como parametros explicitos para manter a clareza.
- Defina os contextos como interfaces, não como classes concretas. Isso facilita a criação de implementacoes de teste e respeita o princípio de inversao de dependência, desacoplando a função de implementacoes especificas.
- Evite acumular mais de tres context receivers em uma única função. Muitos contextos implicitos tornam difícil entender de onde vem cada método chamado e prejudicam a legibilidade do código.
- Documente claramente quais contextos sao necessários e por que. Como os contextos sao implicitos, leitores do código podem não entender imediatamente de onde vem um método. Um comentario breve ou documentação KDoc ajuda bastante.
- Lembre-se de que o recurso e experimental. Isole o uso de context receivers em modulos internos onde mudancas de API sao facilmente propagaveis, evitando expor context receivers em APIs publicas de bibliotecas.
Perguntas Frequentes
P: Context receivers substituem injecao de dependências como Koin ou Dagger? R: Nao. Context receivers resolvem a passagem implicita de contextos em tempo de compilação, enquanto frameworks de DI como Koin e Dagger resolvem a criação e o gerenciamento do ciclo de vida de dependências em tempo de execução. Eles sao complementares: você pode usar DI para criar as instancias e context receivers para disponibiliza-las implicitamente nas funções.
P: Posso usar context receivers em producao se o recurso e experimental? R: Tecnicamente sim, mas com cautela. A sintaxe e o comportamento podem mudar em versões futuras do Kotlin. Se decidir usar, isole o uso em modulos internos e esteja preparado para refatorar quando a API estabilizar. Monitore as KEEPs (Kotlin Evolution and Enhancement Proposals) para acompanhar a direcao do recurso.
P: Qual a diferenca entre context receivers e scope functions como with e run?
R: Scope functions criam um escopo temporario onde this se refere a um objeto, mas isso e uma decisao do ponto de chamada. Context receivers sao parte da assinatura da função e verificados pelo compilador: a função só pode ser chamada quando todos os contextos declarados estao disponiveis. Scope functions sao mecanismo de chamada; context receivers sao mecanismo de declaracao.
P: Como resolver ambiguidade quando dois context receivers tem métodos com o mesmo nome?
R: Use a sintaxe de qualificacao explicita com this@NomeDoTipo para indicar de qual receiver você quer chamar o método. Por exemplo, se LoggerContext e MetricasContext ambos tem um método registrar, use this@LoggerContext.registrar() ou this@MetricasContext.registrar() para desambiguar.
Erros comuns
Esquecer de habilitar a flag do compilador: sem
-Xcontext-receivers, o código não compila e a mensagem de erro pode ser confusa.Abusar de context receivers: usar contextos para tudo transforma o código em algo difícil de rastrear. Se o leitor não consegue entender de onde vem um método, o código ficou implicito demais.
Ambiguidade entre receivers: se dois contextos tem métodos com o mesmo nome, o compilador pode ter dificuldade em resolver qual usar. Use qualificadores explicitos nesses casos.
Ignorar que e experimental: o recurso pode mudar em versões futuras do Kotlin. Use com cautela em código de producao e esteja preparado para adaptar.
Confundir com scope functions:
with,applyeruncriam escopos de receiver, mas não são a mesma coisa que context receivers. Context receivers são verificados em tempo de compilação na assinatura da função.
Termos relacionados
- Receiver: o objeto sobre o qual uma extension function opera, acessivel via
this. - Extension Function: função que adiciona comportamento a um tipo existente, usando um único receiver.
- DSL: Domain Specific Language, onde context receivers permitem criar APIs fluentes com múltiplos contextos.
- Scope Functions:
let,run,with,apply,also– funções que criam escopos temporarios de receiver. - Inline: funções inline combinadas com context receivers podem eliminar overhead de criação de objetos.
Context receivers representam uma evolução significativa na expressividade do Kotlin, permitindo que funções declarem suas dependências de contexto de forma clara e verificada pelo compilador. Embora ainda experimental, o recurso já mostra grande potencial para simplificar APIs e DSLs complexas.