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:

  1. Como o objeto é referenciado: via this (receptor) ou it (argumento)
  2. 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:

FuncaoReferência ao objetoValor de retornoCaso de uso principal
letitResultado da lambdaTransformacoes, null-safety
runthisResultado da lambdaInicializacao + computacao
withthisResultado da lambdaOperacoes em bloco
applythisObjeto contextoConfiguracao de objetos
alsoitObjeto contextoEfeitos 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:

  1. Precisa tratar nulidade? Use ?.let { }
  2. Quer configurar propriedades de um objeto? Use apply
  3. Quer fazer efeitos colaterais (log, validacao)? Use also
  4. Quer computar algo usando metodos do objeto? Use run
  5. Tem um objeto não-nulo e quer operar nele em bloco? Use with

Outra forma de pensar:

  • Precisa do resultado da lambda? let, run ou with
  • Precisa do objeto de volta? apply ou also
  • Quer chamar metodos direto (sem it.)? run, with ou apply
  • Quer um nome explícito para o objeto? let ou also

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.