Se você já escreveu código Kotlin com MutableStateFlow, MutableLiveData ou qualquer padrão reativo, conhece bem o ritual: criar uma propriedade privada mutável prefixada com _ e expor uma versão pública somente leitura. Esse padrão de backing properties funciona, mas polui o código e escala mal. O Kotlin 2.3.0 introduziu os Explicit Backing Fields como feature experimental, e o Kotlin 2.4.0-Beta2 trouxe melhorias significativas rumo à estabilização.

Neste guia, vamos entender o problema que essa feature resolve, como usar na prática e por que ela vai mudar a forma como você escreve propriedades em Kotlin.

O problema das backing properties

Considere um ViewModel típico em um projeto Android com Jetpack Compose:

class UserViewModel : ViewModel() {
    // Backing property mutável (privada)
    private val _userName = MutableStateFlow("")
    // Propriedade pública somente leitura
    val userName: StateFlow<String> = _userName.asStateFlow()

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    private val _errorMessage = MutableStateFlow<String?>(null)
    val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()

    private val _userList = MutableStateFlow<List<User>>(emptyList())
    val userList: StateFlow<List<User>> = _userList.asStateFlow()

    fun loadUsers() {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                _userList.value = repository.getUsers()
            } catch (e: Exception) {
                _errorMessage.value = e.message
            } finally {
                _isLoading.value = false
            }
        }
    }
}

Repare no padrão: para cada estado, você precisa de duas declarações — a mutável com _ e a pública imutável. Com quatro estados, já são oito linhas só de declarações. Em ViewModels complexos com 10+ estados, isso vira um pesadelo de manutenção.

O mesmo problema aparece com coleções. Se você quer expor uma List pública mas manipular uma MutableList internamente:

class TaskRepository {
    private val _tasks = mutableListOf<Task>()
    val tasks: List<Task> get() = _tasks.toList()

    fun addTask(task: Task) {
        _tasks.add(task)
    }
}

Como funcionam os Explicit Backing Fields

Os Explicit Backing Fields introduzem uma sintaxe nova: você declara o campo de apoio diretamente dentro da propriedade usando a keyword field:

class UserViewModel : ViewModel() {
    val userName: StateFlow<String>
        field = MutableStateFlow("")

    val isLoading: StateFlow<Boolean>
        field = MutableStateFlow(false)

    val errorMessage: StateFlow<String?>
        field = MutableStateFlow<String?>(null)

    val userList: StateFlow<List<User>>
        field = MutableStateFlow<List<User>>(emptyList())

    fun loadUsers() {
        viewModelScope.launch {
            isLoading.field.value = true
            try {
                userList.field.value = repository.getUsers()
            } catch (e: Exception) {
                errorMessage.field.value = e.message
            } finally {
                isLoading.field.value = false
            }
        }
    }
}

A diferença é clara: uma única declaração por propriedade. O tipo exposto publicamente é StateFlow<T> (somente leitura), mas internamente a classe tem acesso ao MutableStateFlow via field. O compilador faz o smart cast automaticamente dentro do escopo privado.

Se você vem de Java migrando para Kotlin, essa feature elimina um dos padrões mais verbosos que os devs enfrentam na transição.

Habilitando a feature

Como a feature ainda é experimental, você precisa habilitar o opt-in. No build.gradle.kts:

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xexplicit-backing-fields")
    }
}

Ou via anotação no arquivo:

@file:OptIn(ExperimentalExplicitBackingFields::class)

Exemplos práticos

Coleções com tipo interno diferente

Um caso clássico: expor uma lista imutável enquanto manipula uma lista mutável internamente:

class ShoppingCart {
    val items: List<CartItem>
        field = mutableListOf()

    fun addItem(item: CartItem) {
        field.add(item) // Acesso direto ao MutableList
    }

    fun removeItem(item: CartItem) {
        field.remove(item)
    }

    fun clear() {
        field.clear()
    }
}

Sem explicit backing fields, você precisaria do padrão _items / items com duas propriedades separadas. Agora, uma única propriedade resolve tudo.

Map com acesso controlado

class ConfigStore {
    val configs: Map<String, String>
        field = mutableMapOf()

    fun set(key: String, value: String) {
        field[key] = value
    }

    fun remove(key: String) {
        field.remove(key)
    }

    fun loadDefaults() {
        field.putAll(
            mapOf(
                "theme" to "dark",
                "language" to "pt-br",
                "notifications" to "enabled"
            )
        )
    }
}

Propriedade com validação e tipo interno

Você pode combinar explicit backing fields com getters customizados:

class Temperature {
    val celsius: Double
        field = 0.0
        get() = field

    val fahrenheit: Double
        get() = celsius * 9.0 / 5.0 + 32.0

    fun update(newCelsius: Double) {
        require(newCelsius >= -273.15) { "Temperatura abaixo do zero absoluto" }
        celsius.field = newCelsius
    }
}

SharedFlow com buffer configurável

class EventBus {
    val events: SharedFlow<AppEvent>
        field = MutableSharedFlow<AppEvent>(
            replay = 1,
            extraBufferCapacity = 64
        )

    suspend fun emit(event: AppEvent) {
        field.emit(event)
    }
}

Esse padrão é muito útil em projetos que usam Kotlin Flow para comunicação entre camadas.

Comparação: antes vs depois

Vamos comparar um ViewModel real com e sem explicit backing fields:

Antes (backing properties tradicionais)

class ProductViewModel : ViewModel() {
    private val _products = MutableStateFlow<List<Product>>(emptyList())
    val products: StateFlow<List<Product>> = _products.asStateFlow()

    private val _searchQuery = MutableStateFlow("")
    val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()

    private val _selectedCategory = MutableStateFlow<Category?>(null)
    val selectedCategory: StateFlow<Category?> = _selectedCategory.asStateFlow()

    private val _isRefreshing = MutableStateFlow(false)
    val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()

    // 8 linhas de declarações para 4 estados
}

Depois (explicit backing fields)

class ProductViewModel : ViewModel() {
    val products: StateFlow<List<Product>>
        field = MutableStateFlow<List<Product>>(emptyList())

    val searchQuery: StateFlow<String>
        field = MutableStateFlow("")

    val selectedCategory: StateFlow<Category?>
        field = MutableStateFlow<Category?>(null)

    val isRefreshing: StateFlow<Boolean>
        field = MutableStateFlow(false)

    // 4 declarações claras para 4 estados
}

O resultado é um código 50% mais enxuto, sem convenções de nomes com underscore, e com o compilador garantindo type safety entre o tipo exposto e o tipo interno.

Interação com outras features do Kotlin

Com Sealed Classes

Os explicit backing fields combinam bem com sealed classes para modelar estados de UI:

sealed interface UiState<out T> {
    data object Loading : UiState<Nothing>
    data class Success<T>(val data: T) : UiState<T>
    data class Error(val message: String) : UiState<Nothing>
}

class DataViewModel : ViewModel() {
    val uiState: StateFlow<UiState<List<Item>>>
        field = MutableStateFlow<UiState<List<Item>>>(UiState.Loading)

    fun refresh() {
        viewModelScope.launch {
            uiState.field.value = UiState.Loading
            try {
                val items = repository.fetchItems()
                uiState.field.value = UiState.Success(items)
            } catch (e: Exception) {
                uiState.field.value = UiState.Error(e.message ?: "Erro desconhecido")
            }
        }
    }
}

Com Scope Functions

Você pode usar scope functions normalmente com o campo:

class Logger {
    val entries: List<LogEntry>
        field = mutableListOf()

    fun log(message: String) {
        LogEntry(message, System.currentTimeMillis()).also {
            field.add(it)
        }
    }
}

Quando usar (e quando não usar)

Use explicit backing fields quando:

  • A propriedade pública tem um tipo diferente da representação interna (ex: StateFlow vs MutableStateFlow)
  • Você quer eliminar o padrão _propriedade / propriedade
  • O campo precisa de inicialização específica diferente do tipo exposto

Não use quando:

  • O tipo interno é o mesmo do exposto — val e var tradicionais são suficientes
  • Você precisa de compatibilidade com Kotlin < 2.3.0
  • O projeto ainda não pode usar features experimentais em produção

Status e roadmap

A feature foi introduzida como experimental no Kotlin 2.3.0 e recebeu melhorias no 2.4.0-Beta2. A expectativa é que se torne estável em uma release futura do Kotlin 2.4.x. Para acompanhar, confira o KEEP-430 no repositório oficial.

Se você está testando as novidades do Kotlin 2.4.0-Beta2, confira também nosso artigo sobre Collection Literals, outra feature experimental que chegou nessa versão.

Conclusão

Explicit Backing Fields resolvem um problema real e cotidiano do desenvolvimento Kotlin: a verbosidade das backing properties. Com uma sintaxe limpa e type-safe, a feature elimina duplicação, reduz erros de nomeação e torna ViewModels e repositórios significativamente mais legíveis.

Embora ainda experimental, vale a pena testar em projetos pessoais e módulos internos. Quando estabilizar, será uma das mudanças mais impactantes na forma como escrevemos propriedades em Kotlin.

Para quem trabalha com outras linguagens da JVM, é interessante comparar como cada uma lida com encapsulamento de estado. Em Python, por exemplo, o padrão de properties com @property decorator resolve um problema similar, enquanto em Go o controle de visibilidade é feito por convenção de maiúsculas/minúsculas no nome.