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:
StateFlowvsMutableStateFlow) - 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 —
valevartradicionais 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.