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:
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:
- A private
_userStatefor mutations - A public
userStatefor 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:
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:
import androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport 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:
- Forgot this is experimental: I tried using it without knowing it might change in future Kotlin versions.
- Treated it like delegation: I tried to add a
bykeyword, which caused compilation errors. - 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