Skip to content

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 doing
class 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 _uiState and uiState
  • 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 achieve
class 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 delegated
class MyViewModel : ViewModel() {
val uiState = MutableStateFlow(UiState()) // Error here!
}

The error message was:

Property with backing field cannot be delegated

This 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 location
android {
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:

app/build.gradle.kts
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:

Terminal window
# Clean and rebuild
./gradlew clean
./gradlew build

You 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:

  1. Experimental features require opt-in: Kotlin marks this feature as EXPERIMENTAL, so we need to explicitly enable it
  2. Compiler flags go in specific blocks: The -X prefix indicates this is a compiler flag that needs to go in the compilerOptions block
  3. Backward compatibility: By making it experimental, Kotlin can refine the feature without breaking existing code

Let me test the ViewModel code now:

MyViewModel.kt
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:

MyViewModelTest.kt
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:

Terminal window
./gradlew test

I get:

Task :app:test
MyViewModelTest > test ui state updates PASSED

Perfect! 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