As scope functions sao um dos recursos mais usados e, ao mesmo tempo, mais confusos de Kotlin. Existem cinco delas — let, run, with, apply e also — e todas fazem basicamente a mesma coisa: executam um bloco de codigo no contexto de um objeto. A diferença está nos detalhes: como o objeto é referenciado e o que a funcao retorna.
Neste guia, vamos desmistificar cada uma com exemplos práticos do mundo real.
O que são scope functions?
Scope functions são funcoes da biblioteca padrão do Kotlin que criam um escopo temporário para um objeto. Dentro desse escopo, voce acessa o objeto sem precisar repetir seu nome. Elas existem para tornar o código mais conciso e legível — especialmente quando voce precisa fazer várias operacões em sequência sobre o mesmo objeto.
Todas as cinco funcoes fazem essencialmente o mesmo: executam um bloco de código em um objeto. O que muda entre elas sao dois fatores:
- Como o objeto é referenciado: via
this(receptor) ouit(argumento) - O que é retornado: o resultado da lambda ou o próprio objeto
Tabela comparativa
Antes de mergulhar nos detalhes, aqui está o resumo que voce vai consultar sempre:
| Funcao | Referência ao objeto | Valor de retorno | Caso de uso principal |
|---|---|---|---|
let | it | Resultado da lambda | Transformacoes, null-safety |
run | this | Resultado da lambda | Inicializacao + computacao |
with | this | Resultado da lambda | Operacoes em bloco |
apply | this | Objeto contexto | Configuracao de objetos |
also | it | Objeto contexto | Efeitos colaterais |
Guarde essa tabela. A chave é entender que this permite chamar metodos diretamente (sem prefixo), enquanto it é um nome explícito para o objeto.
let: transformacao e null-safety
A funcao let é provavelmente a scope function mais usada em Kotlin. Ela recebe o objeto como argumento (it) e retorna o resultado da lambda.
O caso de uso classico é com o operador de chamada segura (?.):
val nome: String? = obterNomeDoBanco()
// Sem let — verboso
if (nome != null) {
val formatado = nome.trim().uppercase()
println(formatado)
}
// Com let — idiomático
nome?.let {
val formatado = it.trim().uppercase()
println(formatado)
}
O ?.let só executa o bloco se o valor não for nulo. E uma alternativa elegante ao if (x != null).
Outro uso comum é para transformacoes em cadeia:
val resultado = listaDePedidos
.filter { it.status == Status.PENDENTE }
.let { pedidosPendentes ->
// Renomear 'it' para clareza
println("Encontrados ${pedidosPendentes.size} pedidos pendentes")
pedidosPendentes.sortedBy { it.data }
}
Note que voce pode renomear it para um nome mais descritivo — algo que nao é possível com this.
run: inicializacao + computacao
A funcao run é como o let, mas usa this em vez de it. Isso significa que voce pode chamar metodos do objeto diretamente, sem prefixo. Ela retorna o resultado da lambda.
val resultado = servico.run {
// 'this' é o servico — chamadas diretas
configurar(timeout = 5000)
conectar()
executarQuery("SELECT * FROM usuarios")
}
// resultado = retorno de executarQuery()
O run tambem tem uma versao sem receptor, util para limitar escopo de variaveis:
val hexColor = run {
val r = (0..255).random()
val g = (0..255).random()
val b = (0..255).random()
String.format("#%02x%02x%02x", r, g, b)
}
// r, g, b não vazam para o escopo externo
Esse padrão é excelente para computacoes que precisam de variaveis temporárias sem poluir o escopo ao redor.
with: operacoes em bloco sem null-safety
A funcao with é quase identica ao run, mas com uma diferença importante: ela não é uma extension function. Voce passa o objeto como argumento:
val descricao = with(usuario) {
// 'this' é o usuario
"Nome: $nome, Email: $email, Cidade: $cidade"
}
O with é ideal quando voce tem um objeto não-nulo e quer fazer várias operacoes nele:
with(canvas) {
drawColor(Color.WHITE)
drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
drawText("Kotlin", 100f, 200f, textPaint)
drawCircle(150f, 300f, 50f, circlePaint)
}
A limitacao do with é que ele não funciona com null-safety (?.). Se o objeto pode ser nulo, use run ou let no lugar.
apply: configuracao de objetos
A funcao apply usa this como referência ao objeto, mas diferente do run, ela retorna o próprio objeto em vez do resultado da lambda. Isso a torna perfeita para configurar objetos no estilo builder:
val requisicao = HttpRequest().apply {
url = "https://api.exemplo.com/dados"
method = "POST"
headers["Content-Type"] = "application/json"
headers["Authorization"] = "Bearer $token"
timeout = 30_000
body = """{"chave": "valor"}"""
}
// requisicao é o HttpRequest configurado
O apply é extremamente comum em código Android com Kotlin:
val intent = Intent(context, DetalheActivity::class.java).apply {
putExtra("PRODUTO_ID", produto.id)
putExtra("PRODUTO_NOME", produto.nome)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(intent)
Sempre que voce precisar criar um objeto e configurar suas propriedades em sequência, apply é a escolha certa.
also: efeitos colaterais
A funcao also é a contraparte do apply: ela retorna o próprio objeto, mas usa it em vez de this. Ela é ideal para efeitos colaterais — acoes que voce quer executar “de passagem” sem alterar a cadeia de operacoes:
val usuario = criarUsuario("Maria")
.also { println("Usuario criado: ${it.nome}") }
.also { analytics.registrar("novo_usuario", it.id) }
.also { validador.verificar(it) }
O also é perfeito para logging e depuracao em cadeias de chamadas:
val numeros = listOf(1, 5, 3, 2, 4)
.also { println("Original: $it") }
.sorted()
.also { println("Ordenado: $it") }
.reversed()
.also { println("Invertido: $it") }
Voce pode inserir chamadas also em qualquer ponto de uma cadeia para inspecionar valores intermediários — e removê-las depois sem afetar a lógica.
Quando usar qual: arvore de decisao
Na dúvida sobre qual scope function escolher, siga este fluxo:
- Precisa tratar nulidade? Use
?.let { } - Quer configurar propriedades de um objeto? Use
apply - Quer fazer efeitos colaterais (log, validacao)? Use
also - Quer computar algo usando metodos do objeto? Use
run - Tem um objeto não-nulo e quer operar nele em bloco? Use
with
Outra forma de pensar:
- Precisa do resultado da lambda?
let,runouwith - Precisa do objeto de volta?
applyoualso - Quer chamar metodos direto (sem
it.)?run,withouapply - Quer um nome explícito para o objeto?
letoualso
Erros comuns e anti-patterns
As scope functions melhoram a legibilidade quando usadas com moderacao. Mas o abuso delas cria código mais difícil de ler do que o original.
Nesting excessivo — o erro mais comum:
// RUIM — difícil de ler
usuario?.let { u ->
u.endereco?.let { e ->
e.cidade?.let { c ->
println("Cidade: $c")
}
}
}
// BOM — simples e direto
val cidade = usuario?.endereco?.cidade
if (cidade != null) {
println("Cidade: $cidade")
}
Usar scope function sem necessidade:
// RUIM — let desnecessário
val tamanho = lista.let { it.size }
// BOM — direto
val tamanho = lista.size
Misturar retornos: tenha cuidado com apply e run dentro da mesma cadeia — os retornos diferentes podem causar bugs sutis.
A regra de ouro: se a scope function não torna o código mais claro, não use. Código explícito sempre vence código “esperto”.
Exemplos do mundo real
Para consolidar, veja como as scope functions aparecem em código de produção:
Configuracao de cliente HTTP com apply:
val client = OkHttpClient.Builder().apply {
connectTimeout(30, TimeUnit.SECONDS)
readTimeout(30, TimeUnit.SECONDS)
addInterceptor(loggingInterceptor)
if (BuildConfig.DEBUG) {
addInterceptor(debugInterceptor)
}
}.build()
Processamento de resposta com let:
fun buscarUsuario(id: Int): UsuarioDTO? {
return repositorio.findById(id)?.let { entidade ->
UsuarioDTO(
nome = entidade.nome,
email = entidade.email,
ativo = entidade.deletadoEm == null
)
}
}
Logging condicional com also:
fun processarPagamento(pedido: Pedido): Resultado {
return gateway.cobrar(pedido.valor)
.also { resultado ->
if (resultado.sucesso) {
logger.info("Pagamento aprovado: ${pedido.id}")
} else {
logger.warn("Pagamento recusado: ${pedido.id} - ${resultado.motivo}")
}
}
}
Conclusao
As scope functions sao ferramentas poderosas que, quando bem usadas, tornam o código Kotlin mais conciso e expressivo. A chave é entender as duas dimensões: como o objeto é referenciado (this vs it) e o que é retornado (resultado da lambda vs objeto contexto).
Com a prática, a escolha se torna intuitiva. Comece com let para null-safety, apply para configuracao de objetos e also para logging. Conforme ganhar confiança, incorpore run e with no seu repertório. E lembre-se: se o código fica mais confuso com scope function, é melhor sem ela.
Para aprofundar seus conhecimentos na linguagem, confira nosso guia sobre o que é Kotlin e o tutorial de extension functions, que complementam perfeitamente o entendimento das scope functions. Se você programa em outras linguagens, é interessante comparar: Rust não tem scope functions, mas resolve encadeamento com pattern matching e closures, enquanto Python usa context managers (with) para um propósito similar.