---
title: "Kotlin 2.4: As Três Mudanças que Moveram Minha Mão no Teclado"
url: https://tiarebalbi.com/pt-br/blog/kotlin-2-4-three-changes-worth-using
markdown: https://tiarebalbi.com/pt-br/blog/kotlin-2-4-three-changes-worth-using.md
description: "O Kotlin 2.4 estabiliza context parameters e explicit backing fields, e remove o compilador K1. Um recorte backend do que usar hoje e do que agendar."
author: "Tiarê Balbi Bonamini"
locale: pt-br
published: 2026-06-08
updated: 2026-06-08
category: "Engineering"
tags: ["kotlin", "kotlin-2.4", "context-parameters", "explicit-backing-fields", "compilador-k2", "backend-engineering", "jvm"]
translation: https://tiarebalbi.com/en/blog/kotlin-2-4-three-changes-worth-using
---
# Kotlin 2.4: As Três Mudanças que Moveram Minha Mão no Teclado

O Kotlin 2.4.0 foi lançado este mês, e as notas de release são longas: context parameters estáveis, explicit backing fields, annotation use-site targets, API UUID estável, verificações de ordenação, suporte a Java 26 e a remoção do compilador K1, entre outros. Li a lista inteira. Depois, voltei para alguns dos meus próprios arquivos e os reescrevi contra o 2.4 para ver quais linhas realmente mudaram.

A resposta honesta: três recursos mudaram a forma como eu digito, e uma remoção mudou como eu organizei minha semana. Todo o resto é uma nota de rodapé que vou apreciar no dia em que esbarrar nela. Este é o recorte opinativo, escrito da cadeira de um engenheiro backend em vez de um Android.

## Context parameters: passar o logger sem passar o logger

Este é o que eu vou buscar primeiro. Os context parameters se tornaram Estáveis no 2.4 (issue de rastreamento KT-72222), substituindo o experimento mais antigo de context receivers.

O problema que eles resolvem é a dependência que toda função em um caminho de chamada precisa, mas ninguém quer na assinatura. Um logger com escopo de requisição é o exemplo mais limpo. Um clock, um handle de transação ou um tenant id funcionam da mesma forma. Você ou passa o valor por toda função como um argumento que ela na verdade não usa, ou recorre a uma thread-local e perde toda a segurança de tipos.

Um context parameter é uma terceira opção: a função declara que precisa de um valor de algum tipo de seu entorno, e o compilador o fornece no call site sem você escrever o argumento.

Aqui está um arquivo autocontido. Ele compila e roda no Kotlin 2.4.0 sem flags.

```kotlin
import java.util.UUID

// Uma coisa que todo handler precisa mas ninguém quer na assinatura:
// um logger com escopo de requisição que marca linhas com o request id.
class RequestLog(val requestId: UUID) {
    fun info(message: String) = println("[$requestId] $message")
}

data class Order(val id: String, val cents: Long)

// O context parameter diz: "Preciso de um RequestLog do meu entorno."
// Os chamadores nunca passam explicitamente; o compilador resolve por tipo.
context(log: RequestLog)
fun charge(order: Order): Long {
    log.info("charging order ${order.id} for ${order.cents}c")
    return order.cents
}

context(log: RequestLog)
fun refund(order: Order): Long {
    log.info("refunding order ${order.id}")
    return -order.cents
}

// Um único `with(log)` abre o escopo; tudo dentro vê o logger.
fun handleRequest(order: Order) {
    val log = RequestLog(UUID.nameUUIDFromBytes("req-42".toByteArray()))
    with(log) {
        val net = charge(order) + refund(order)
        log.info("net effect: ${net}c")
    }
}

fun main() {
    handleRequest(Order("A-100", 2599))
}
```

Execute com `kotlinc orders.kt -include-runtime -d orders.jar && java -jar orders.jar`. Ele imprime três linhas, cada uma marcada com o mesmo request id, e `charge` e `refund` nunca nomearam o logger na chamada.

A linha que vale estudar é `context(log: RequestLog)`. O nome `log` torna a dependência referenciável dentro do corpo, algo que os context receivers sem nome não conseguiam fazer de forma limpa. No call site, `charge(order)` não carrega logger; o bloco `with(log)` colocou um `RequestLog` em escopo, e o compilador o casou por tipo.

Esse casamento é a parte que você precisa entender antes de se comprometer com o recurso. Pela minha leitura do código de resolução FIR, a resolução de contexto é uma busca real por escopo orientada por tipo, não um truque sintático. Para cada context parameter, o compilador conta os valores correspondentes em escopo e decide pela contagem.

​

![](https://rso2zax703psuq0y.public.blob.vercel-storage.com/postscontent/1780889888410-54tkdprrjcg.jpeg)

O diagrama acima é todo o modelo mental. Zero valores do tipo certo em escopo, e a chamada reporta `NoContextArgument` e não compila. Exatamente um, e ele é passado. Dois ou mais, e você recebe `AmbiguousContextArgument` — o compilador se recusa a adivinhar entre dois loggers igualmente válidos. Essa é a proteção: uma dependência implícita que está faltando ou é ambígua é um erro de compilação, nunca uma escolha silenciosa em tempo de execução.

O trade-off é a legibilidade, e é real. Um leitor escaneando `charge(order)` não consegue ver que um logger está fluindo. Você trocou um argumento explícito-mas-barulhento por um invisível-mas-quieto. Minha regra ao reescrever um handler assim: context parameters valem a pena para valores transversais que genuinamente atravessam um grafo de chamadas profundo — um logger, um clock, uma transação. No momento em que dois valores do mesmo tipo podem coexistir em escopo, você está a um refactor de um erro de ambiguidade, e a implicitude para de compensar.

Uma ressalva específica para o 2.4: nomear o argumento no call site, como `charge(log = primary)`, ainda é experimental atrás de `-Xexplicit-context-arguments`. O recurso de linguagem que liga isso por padrão está previsto para o 2.5. Então no 2.4 você tem a forma implícita estável, mas ainda não pode desambiguar por nome sem a flag opt-in.

## Explicit backing fields aposentam a propriedade shadow privada

A segunda mudança que continuo usando é explicit backing fields, também Estável no 2.4. O número do tracker, KT-14663, é uma das requests abertas mais antigas no Kotlin, o que mostra quanto tempo o padrão que ele elimina existe.

O padrão é a propriedade que você expõe como um tipo read-only enquanto seu backing field guarda um tipo mais amplo e mutável. No Android é o par `_state` / `state` `StateFlow`, mas o formato aparece em muito código backend: um buffer de métricas, uma lista acumuladora, qualquer coisa internamente mutável e read-only para chamadores. O modo antigo precisa de duas declarações:

```kotlin
private val _items = mutableListOf<Int>()
val items: List<Int> get() = _items
```

Dois nomes para um pedaço de estado, só porque o tipo interno e o tipo externo diferem. Explicit backing fields colapsam isso em uma única propriedade:

```kotlin
class Counter {
    val total: List<Int>
        field = mutableListOf<Int>()

    fun add(n: Int) { total.add(n) }   // dentro: MutableList<Int>
}
```

Compilei e executei exatamente isso no 2.4.0. Dentro da classe, `total` resolve para `MutableList<Int>`, então `total.add(n)` funciona. Fora, `total` é `List<Int>` e não expõe nenhuma mutação. Uma propriedade, duas visibilidades do mesmo objeto, sem propriedade shadow prefixada com `_`.

As restrições valem conhecer antes de buscar o recurso. Pela minha leitura do checker, um explicit backing field é permitido apenas em um `val` final, nunca em um `var`, propriedade `open`, ou em propriedades de interface, abstratas, expect ou extension. A visibilidade do backing field deve ser mais restritiva do que a da propriedade. A lógica é consistência: se a propriedade pudesse ser sobrescrita, uma subclasse poderia fornecer um tipo de campo diferente, e um chamador não conseguiria mais raciocinar sobre qual tipo ela carrega. A restrição a `val` final remove essa ambiguidade. Para código backend isso aterrissa menos frequentemente do que para um Android ViewModel, mas toda vez que tenho uma propriedade "mutável dentro, imutável fora", a shadow agora se foi.

## Name-based destructuring, ainda atrás de uma flag

A terceira mudança não foi lançada como estável, e quero ser preciso sobre isso porque é fácil interpretar mal a cobertura da release. Name-based destructuring é experimental no 2.4, atrás de `-Xname-based-destructuring`. Estou incluindo porque ela já mudou como eu escrevo novos consumidores de data class em branches em que estou disposto a usar feature flag, e porque corrige uma classe de bug que já vivi de verdade.

O destructuring posicional vincula por ordem, não por nome. A armadilha clássica:

```kotlin
data class Person(val name: String, val age: Int)
val (age, name) = person   // compila, ambos estão errados
```

Isso compila. `age` recebe o nome e `name` recebe a idade, porque posição é tudo o que o compilador usa. Adicione um campo no meio da data class depois, e todo destructuring posicional downstream silenciosamente se desloca.

Name-based destructuring vincula pelo nome da propriedade. Verifiquei isso no 2.4.0 com `-Xname-based-destructuring=only-syntax`:

```kotlin
(val age, val name) = person   // vincula por nome, ordem irrelevante
```

Aqui `age` recebe `person.age` e `name` recebe `person.name`, independentemente da ordem em que escrevi ou da ordem em que a data class declara. O bytecode é inalterado; este é um recurso do call site. A flag tem três modos que vale conhecer: `only-syntax` habilita a nova forma parentizada, `name-mismatch` avisa quando um destructuring posicional antigo usa nomes que não combinam com as propriedades, e `complete` liga a forma curta. A JetBrains sinalizou estabilização para uma release posterior.

Ainda não estou colocando isso no `main`. Mas só o modo `name-mismatch` vale um experimento de CI: ele expõe todo destructuring posicional existente em que os nomes das variáveis já mentem sobre qual campo elas carregam.

## A remoção do K1 que tive que agendar

A mudança breaking no 2.4 é estrutural: o frontend do compilador K1 sumiu. Desde o 2.0, o K2 é o padrão, mas o K1 persistia atrás de `-language-version 1.9` para projetos que precisavam dele. No 2.4 essa saída de emergência foi removida. O compilador agora rejeita `-language-version 1.9` diretamente; a versão de linguagem mais baixa que aceita é 2.0.

Se seu build ainda fixa `languageVersion = "1.9"` em algum lugar — uma convenção Gradle, um módulo teimoso, defaults de um plugin de terceiros — o 2.4 não vai compilá-lo. Esse é o item de linha que tive que colocar no calendário em vez de absorver no dia do upgrade. A correção não é difícil, mas não é automática: encontre toda versão de linguagem fixada, abaixe para pelo menos 2.0, e recompile contra a resolução mais estrita do K2, que pega algumas construções que o K1 deixava passar.

Para usuários de Kotlin Multiplatform há uma mudança pareada: partial linkage agora está permanentemente ativo. A flag `-Xpartial-linkage` está depreciada, e apenas `-Xpartial-linkage-loglevel` sobrevive para controlar quão barulhento um link degradado fica. Se você dependia de desligar partial linkage, esse switch sumiu.

O restante das adições da biblioteca padrão do 2.4 são as notas de rodapé que mencionei. `isSorted`, `isSortedBy` e `isSortedDescending` chegam em iteráveis, arrays e sequences; eles fazem short-circuit no primeiro par fora de ordem em vez de varrer a coleção inteira. O tipo `kotlin.uuid.Uuid` agora está estável, então parsear e formatar UUIDs não precisa de opt-in — embora `generateV4` e `generateV7` para criar novos ainda sejam experimentais. `UInt.toBigInteger()` e `ULong.toBigInteger()` substituem as antigas conversões baseadas em string na JVM. Bom de ter, nenhum deles mudou uma linha que eu já tinha escrito.

## O que realmente fazer com isso

* Busque **context parameters** para um valor transversal que atravessa um grafo de chamadas profundo — logger, clock, transação. Pare no momento em que dois valores do mesmo tipo possam compartilhar um escopo, ou você trocará código quieto por um erro `AmbiguousContextArgument`.
* Use **explicit backing fields** onde quer que tenha um `val` final que é mutável dentro e read-only fora. Apague a propriedade shadow prefixada com `_`. Lembre-se de que é apenas para `val` final.
* Experimente **name-based destructuring** no modo `name-mismatch` em CI antes de adotar. Ele sinaliza todo destructuring posicional cujos nomes de variáveis já discordam da data class.
* Antes de atualizar, **faça grep por `languageVersion = "1.9"`** em todo o seu build e abaixe para 2.0+. O K1 sumiu; um 1.9 fixado quebra o build.

Quando evitar cada um: pule context parameters para dependências pontuais que uma única função precisa — um argumento simples é mais claro. Pule explicit backing fields em propriedades `var` ou `open`; o compilador não vai deixar de qualquer forma. Mantenha name-based destructuring fora do `main` até estabilizar. E não trate a remoção do K1 como opcional — é a única mudança no 2.4 que falha de forma fechada.
