Skip to content

How to Use Kotlin 2.3 Explicit Backing Fields

Purpose

This post demonstrates how to use Kotlin 2.3 Explicit Backing Fields to eliminate the dual property boilerplate that has plagued Android developers for years.

Environment

  • Kotlin 2.3.0 (experimental feature)
  • Android Studio
  • StateFlow for UI state management

The Problem

When I was building Android ViewModels, I got tired of writing this dual property pattern:

UserProfileViewModel.kt
class UserProfileViewModel : ViewModel() {
private val _userState = MutableStateFlow<UserState>(Loading)
val userState: StateFlow<UserState> = _userState
fun refreshUser() {
_userState.value = Loading // Must access private backing field
}
}

The worst part? I had to maintain two separate properties:

  1. A private _userState for mutations
  2. A public userState for observation

And when I tried to use smart-casts, it failed:

fun updateUser(viewModel: UserProfileViewModel) {
if (viewModel._userState.value is Loading) { // ERROR: private access
// Can't directly access _userState
}
}

This made my code harder to read and maintain.

The Solution

Then I discovered Kotlin 2.3 introduces explicit backing fields with the field = syntax. I tried this new approach:

UserProfileViewModel.kt
class UserProfileViewModel : ViewModel() {
val userState: StateFlow<UserState> field = MutableStateFlow(Loading)
fun refreshUser() {
field.value = Loading // Can directly access backing field
}
}

It’s so much cleaner! The compiler automatically creates a private backing field and exposes the public getter.

How It Works

I tested this with a real Android ViewModel:

MainActivityViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
data class UiState(
val isLoading: Boolean = false,
val data: List<String> = emptyList()
)
class MainActivityViewModel : ViewModel() {
val uiState: StateFlow<UiState> field = MutableStateFlow(UiState())
fun loadData() {
// Update the backing field directly
field.value = field.value.copy(isLoading = true)
viewModelScope.launch {
try {
// Simulate network call
val newData = fetchDataFromApi()
field.value = UiState(data = newData)
} catch (e: Exception) {
field.value = field.value.copy(isLoading = false)
}
}
}
private suspend fun fetchDataFromApi(): List<String> {
delay(1000) // Simulate delay
return listOf("Item 1", "Item 2", "Item 3")
}
}

When I run this ViewModel in my Android app, it works perfectly. The UI can observe uiState and I can update it directly using field.value.

Smart-Cast Benefits

I also tested smart-casts and they work automatically now:

fun updateUI(viewModel: MainActivityViewModel) {
if (viewModel.uiState.value.isLoading) { // Works perfectly!
// Smart-cast applies to the backing field
showLoadingIndicator()
}
}

No more private access errors!

Syntax Comparison

Here’s how the old way compares to the new way:

// Old way - 2 properties (What I used before)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState
// New way - 1 property (What I use now)
val uiState: StateFlow<UiState> field = MutableStateFlow(UiState())

The new syntax reduces my boilerplate by 50%. Less code to maintain, fewer naming conflicts, and better readability.

Common Mistakes

When I first tried this feature, I made some mistakes:

  1. Forgot this is experimental: I tried using it without knowing it might change in future Kotlin versions.
  2. Treated it like delegation: I tried to add a by keyword, which caused compilation errors.
  3. Used it with non-properties: I tried applying it to functions and variables, which doesn’t work.

The key takeaway: Only use field = with property declarations.

Why This Matters

I think the key reason this feature is so important is:

  • Reduced boilerplate: Half the code for state management
  • Better readability: Single property instead of dual properties
  • Automatic smart-casts: No more private access workaround
  • Type safety: Still compile-time checked
  • ViewModel-friendly: Perfect for Android development

Summary

In this post, I showed how to use Kotlin 2.3 explicit backing fields to eliminate dual property boilerplate in ViewModels. The key point is the field = syntax which automatically creates a private backing field while exposing a public getter. This reduces code by 50% and enables automatic smart-casts, making Android development much more enjoyable.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments