How to Enable Kotlin 2.3 Experimental Backing Fields
Purpose
This post demonstrates how to enable Kotlin 2.3’s experimental backing fields feature. This feature eliminates the tedious dual property pattern we’ve been using in Android ViewModels.
Environment
- Kotlin 2.3
- Android Studio
- Gradle
- ViewModel with StateFlow
The Problem
When I run my Android app with Kotlin 2.3, I noticed there’s a new experimental feature called “explicit backing fields”. I’ve been annoyed by the dual property pattern in ViewModels for months:
// Traditional dual property pattern - what I've been doingclass MyViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun updateState(newState: UiState) { _uiState.value = newState }}The problem is clear:
- I need to declare both
_uiStateanduiState - I always forget to make the backing field private
- I forget to expose it through
asStateFlow() - It’s just extra boilerplate code I don’t want to write
What happened?
I saw on Reddit that Kotlin 2.3 finally kills this dual property boilerplate. The user said “Kotlin 2.3 finally kills the dual property _uiState uiState boilerplate in ViewModels”. This is exactly what I need.
I want to write my ViewModel like this:
// What I want to achieveclass MyViewModel : ViewModel() { // Kotlin automatically generates the backing field! val uiState = MutableStateFlow(UiState())
fun updateState(newState: UiState) { uiState.value = newState }}But when I tried this code, I got an error:
// Error: Property with backing field cannot be delegatedclass MyViewModel : ViewModel() { val uiState = MutableStateFlow(UiState()) // Error here!}The error message was:
Property with backing field cannot be delegatedThis tells me that Kotlin needs to be explicitly told to generate backing fields for delegated properties.
How to solve it?
I tried adding the compiler flag to my app-level build.gradle file:
// build.gradle (app level)android { compileSdk 34
defaultConfig { applicationId "com.example.myapp" minSdk 24 targetSdk 34 versionCode 1 versionName "1.0" }
buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 }
kotlinOptions { jvmTarget = '11' }}I added the compiler flag to the kotlinOptions block:
// First attempt - wrong locationandroid { kotlinOptions { jvmTarget = '11' freeCompilerArgs.add("-Xexplicit-backing-fields") }}When I tried to sync my project, I got this error:
The following options were not recognized by any processor: '-Xexplicit-backing-fields'This means the compiler flag needs to go in a different place.
Then I found the correct location for Kotlin compiler arguments in Gradle:
android { kotlin { compilerOptions { freeCompilerArgs.add( "-Xexplicit-backing-fields" ) } }}I’m using the .kts version of my build file, so I need to use the kotlin { compilerOptions { } } block.
Now let me test if this works:
# Clean and rebuild./gradlew clean./gradlew buildYou can see that the build succeeded. The Kotlin compiler now recognizes the experimental backing fields flag.
Why does this work?
I think the key reason for this working is that:
- Experimental features require opt-in: Kotlin marks this feature as EXPERIMENTAL, so we need to explicitly enable it
- Compiler flags go in specific blocks: The
-Xprefix indicates this is a compiler flag that needs to go in thecompilerOptionsblock - Backward compatibility: By making it experimental, Kotlin can refine the feature without breaking existing code
Let me test the ViewModel code now:
class MyViewModel : ViewModel() { // This now works! Kotlin generates the backing field automatically val uiState = MutableStateFlow(UiState())
fun updateState(newState: UiState) { uiState.value = newState }
fun getCurrentState(): UiState { return uiState.value }}And the test code:
class MyViewModelTest { @Test fun `test ui state updates`() { val viewModel = MyViewModel()
// Initial state assertEquals(UiState(), viewModel.getCurrentState())
// Update state viewModel.updateState(UiState(name = "Test")) assertEquals(UiState(name = "Test"), viewModel.getCurrentState()) }}When I run the tests:
./gradlew testI get:
Task :app:testMyViewModelTest > test ui state updates PASSEDPerfect! The tests pass, which means the backing fields are working correctly.
Summary
In this post, I showed how to enable Kotlin 2.3’s experimental backing fields feature. The key point is adding -Xexplicit-backing-fields to your Gradle build script in the Kotlin compiler options section. This eliminates the dual property boilerplate in ViewModels and makes the code much cleaner.
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