Kotlin 2.3.0: Say Goodbye to Backing Properties with Explicit Backing Fields
Ever found yourself writing private mutable properties alongside public read-only ones just to manage state? If you've worked with StateFlow, LiveData, or any reactive pattern in Kotlin, you know the drill. Well, Kotlin 2.3.0 just made your life easier with Explicit Backing Fields – and trust me, once you try it, there's no going back.
The Old Way: The Backing Property Pattern
Let's face it – we've all written code like this countless times:
private val _city = MutableStateFlow<String>("")
val city: StateFlow<String> get() = _city
fun updateCity(newCity: String) {
_city.value = newCity
}
It works, but it's verbose. You're essentially maintaining two properties for one logical piece of state. Plus, you need to remember the naming convention (usually prefixing with _), and your code gets cluttered fast when you have multiple state properties.
The New Way: Explicit Backing Fields
Kotlin 2.3.0 introduces a cleaner syntax that eliminates the need for separate private properties:
val city: StateFlow<String>
field = MutableStateFlow("")
fun updateCity(newCity: String) {
// Smart casting works automatically!
city.value = newCity
}
Wait, what just happened? 🤔
The field keyword now lets you explicitly declare the backing field's type right within the property declaration. The compiler automatically smart-casts field to MutableStateFlow within the same private scope, so you can call city.value = newCity directly!
Why This Matters
1. Less Boilerplate
No more maintaining parallel properties. One declaration, one concern.
2. Automatic Smart Casting
The compiler knows that within your class, city is actually a MutableStateFlow, so you can access mutable methods without casting.
3. Cleaner API Surface
External consumers see only the read-only StateFlow<String>, while your class internals can mutate it freely.
Real-World Example: ViewModel
Let's see a practical Android ViewModel example:
Before (Traditional Backing Properties)
class UserViewModel : ViewModel() {
private val _username = MutableStateFlow("")
val username: StateFlow<String> = _username
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
private val _errors = MutableStateFlow<List<String>>(emptyList())
val errors: StateFlow<List<String>> = _errors
fun updateUsername(name: String) {
_username.value = name
}
fun setLoading(loading: Boolean) {
_isLoading.value = loading
}
fun addError(error: String) {
_errors.value = _errors.value + error
}
}
After (Explicit Backing Fields)
class UserViewModel : ViewModel() {
val username: StateFlow<String>
field = MutableStateFlow("")
val isLoading: StateFlow<Boolean>
field = MutableStateFlow(false)
val errors: StateFlow<List<String>>
field = MutableStateFlow(emptyList())
fun updateUsername(name: String) {
username.value = name // Smart cast to MutableStateFlow
}
fun setLoading(loading: Boolean) {
isLoading.value = loading
}
fun addError(error: String) {
errors.value = errors.value + error
}
}
Look at that! Three private properties eliminated, and the code is much more readable.
Another Use Case: Collections
Explicit backing fields shine when exposing immutable collections backed by mutable ones:
class ShoppingCart {
val items: List<String>
field = ArrayList()
fun addItem(item: String) {
items.add(item) // Smart cast to ArrayList
}
fun removeItem(item: String) {
items.remove(item)
}
fun clear() {
items.clear()
}
}
External code sees items as a List<String> (read-only), but internally, you work with the full power of ArrayList.
How to Enable It
Since this feature is experimental in Kotlin 2.3.0, you need to opt in. Add this to your build.gradle.kts:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexplicit-backing-fields")
}
}
Or if you're using the older Gradle syntax:
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xexplicit-backing-fields"
}
}
Things to Keep in Mind
Scope Matters: The smart casting only works within the same class. Outside code still sees the exposed type.
Experimental Feature: This is currently experimental, so expect potential changes before it becomes stable.
Use Case: Best suited for situations where you need different internal and external types for the same logical property.
When NOT to Use It
- If you genuinely need two separate properties with independent logic
- When the backing property needs to be accessed from subclasses (backing fields are private to the declaring class)
- If your team isn't on Kotlin 2.3.0+ yet (though you should be! 😉)
The Bottom Line
Explicit backing fields are one of those features that make you wonder, "Why didn't we have this earlier?" It's a small change that significantly improves code readability and reduces boilerplate, especially in reactive programming patterns.
If you're building Android apps with StateFlow, Compose state, or any pattern that requires exposing immutable views of mutable data, this feature is a game-changer.
Give it a try in your next project, and let the compiler do the heavy lifting. Your future self (and your code reviewers) will thank you!
Quick Reference
// ✅ DO: Use explicit backing fields for state encapsulation
val state: StateFlow<String>
field = MutableStateFlow("")
// ✅ DO: Use it for collection encapsulation
val items: List<Item>
field = mutableListOf()
// ❌ DON'T: Use it when you need independent properties
// Use traditional backing properties instead
Have you tried explicit backing fields yet? Share your experience or questions in the comments below! And if you're still on an older Kotlin version, now might be the perfect time to upgrade and explore all the goodies Kotlin 2.3.0 has to offer.
Happy Kotlin coding! 🚀
Need an Android Developer or a full-stack website developer?
I specialize in Kotlin, Jetpack Compose, and Material Design 3. For websites, I use modern web technologies to create responsive and user-friendly experiences. Check out my portfolio or get in touch to discuss your project.


