Val vs Var in Kotlin: Immutable and Mutable Properties Explained for Android Developers

Use val for read-only properties that never change after initialization, and var for mutable properties that need reassignment; prefer val by default for safer, thread-safe Android code.

In the JetBrains/kotlin repository, the distinction between val and var is enforced at every level of the compiler pipeline—from lexical tokens to bytecode generation. Understanding this core difference is essential for writing robust Android applications that leverage Kotlin's immutability guarantees.

What Is the Difference Between Val and Var in Kotlin?

Kotlin uses two distinct keywords to control mutability at the property level. The compiler treats these as fundamentally different constructs, generating different bytecode and enforcing different constraints.

Val: Read-Only References

A val declares a read-only property. You can assign it a value exactly once—either at declaration time, in an init block, or inside a constructor. After initialization, the reference cannot be reassigned.

Under the hood, the Kotlin compiler creates a PropertyDescriptorImpl with isVar = false (see core/descriptors/src/org/jetbrains/kotlin/descriptors/impl/PropertyDescriptorImpl.java line 68). At the JVM level, this translates to a final field with no setter generated.

Var: Mutable References

A var declares a mutable property. You can reassign its value any number of times after the initial assignment. The compiler generates both a getter and a setter for the property.

In the descriptor system, var properties are flagged with isVar = true (same file, line 68). The JVM backend generates a non-final backing field and a setter method, allowing runtime reassignment.

How Kotlin Implements Val vs Var at the Compiler Level

The distinction between val and var is not merely syntactic sugar; it propagates through every phase of the Kotlin compiler.

Lexical Analysis and Tokenization

During the initial lexing phase, the Kotlin compiler identifies property declarations using distinct tokens. In compiler/multiplatform-parsing/common/src/org/jetbrains/kotlin/kmp/lexer/KtTokens.kt, line 18 defines VAL_KEYWORD for val, while line 19 defines VAR_KEYWORD for var. These tokens determine how the parser constructs the property's abstract syntax tree.

Descriptor and Symbol Resolution

When the compiler builds property descriptors for semantic analysis, it instantiates PropertyDescriptorImpl with the isVar boolean flag. This flag persists through the Analysis API, where KaPropertySymbol.isVal reflects the original declaration choice. The renderer in analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/renderer/declarations/renderers/callables/KaKotlinPropertySymbolRenderer.kt (line 35) selects KtTokens.VAL_KEYWORD or VAR_KEYWORD based on this flag when displaying property signatures.

Bytecode Generation

At the backend, the isVar flag determines JVM bytecode output:

  • For val: The backing field is marked final, and no setter is generated.
  • For var: The backing field is non-final, and the compiler generates a setter method.

This guarantees that the immutability contract is enforced at the JVM level, not just at compile-time.

Val vs Var in Android Development: Best Practices

Android applications benefit significantly from Kotlin's immutability model. The Android framework's lifecycle and threading model make the choice between val and var particularly important.

When to Use Val in Android

Prefer val as your default choice for properties. It communicates intent clearly and eliminates an entire class of threading bugs.

Compile-time constants: Use const val for configuration keys, request codes, and API endpoints that never change.

const val REQUEST_CODE_CAMERA = 1001
const val BASE_URL = "https://api.example.com/"

Dependency injection: Constructor-injected dependencies should always be val since they never change after the object is created.

class UserRepository @Inject constructor(
    private val api: UserApi,
    private val dao: UserDao
)

Immutable UI state: When using StateFlow or LiveData, declare the flow itself as val while mutating the internal value.

class MainViewModel : ViewModel() {
    // The reference never changes - val
    val uiState: StateFlow<UiState> = MutableStateFlow(UiState.Initial)
    
    fun setLoading(isLoading: Boolean) {
        // Mutating the value inside the flow, not the reference
        (uiState as MutableStateFlow).value = uiState.value.copy(isLoading = isLoading)
    }
}

When to Use Var in Android

Use var only when the property's value must change after initialization.

Mutable UI counters and flags: Fragment or Activity-level state that updates during user interaction.

class CounterFragment : Fragment(R.layout.fragment_counter) {
    private var clickCount = 0  // Must increment, so var required
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        view.findViewById<Button>(R.id.button).setOnClickListener {
            clickCount++
            updateUI()
        }
    }
}

Late-initialized properties: Android's view binding and dependency injection often require lateinit var because the value is set after object construction but before use.

class DetailsFragment : Fragment() {
    private lateinit var binding: FragmentDetailsBinding  // Set in onCreateView
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentDetailsBinding.inflate(inflater, container, false)
        return binding.root
    }
}

Mutable collections: When you need to add or remove elements from a collection held as a property.

class TaskManager {
    private var tasks = mutableListOf<Task>()  // List contents change, reference might too
    
    fun addTask(task: Task) {
        tasks.add(task)
    }
}

Thread Safety and Performance Implications

The choice between val and var directly impacts your Android app's stability and performance.

Thread safety: Because a val property's reference never changes, it is inherently thread-safe to share across coroutines, background threads, and the main thread. The object itself might have mutable state, but the reference stability eliminates race conditions during reassignment. In contrast, reassigning a var from multiple threads without synchronization leads to non-deterministic behavior and potential memory visibility issues.

Performance: On the JVM, val properties compile to final fields. The JIT compiler can inline final fields more aggressively, reducing indirection and improving cache locality. var properties require generated setter methods and non-final fields, adding slight runtime overhead and preventing certain optimizations.

Summary

  • val creates a read-only property with a single assignment; the compiler generates a final field and no setter, making it thread-safe by default.
  • var creates a mutable property allowing multiple reassignments; the compiler generates a setter and marks the field as non-final.
  • Default to val in Android projects for dependency injection, UI state holders, and constants to leverage immutability guarantees.
  • Use var only for counters, late-initialized bindings, and collections that must change after construction.
  • The distinction is enforced at the compiler level through PropertyDescriptorImpl.isVar, lexer tokens in KtTokens.kt, and JVM bytecode generation.

Frequently Asked Questions

Can I change the value of a val property in Kotlin?

No, you cannot reassign a val property after its initial assignment. The compiler enforces this at the semantic analysis phase by setting isVar = false in the property descriptor. However, if the val holds a reference to a mutable object (like a MutableList), the internal state of that object can change even though the reference itself remains constant.

Why does Kotlin recommend using val by default?

Kotlin encourages val by default to promote immutability, which reduces bugs related to state changes and makes code easier to reason about. According to the Kotlin compiler source in PropertyDescriptorImpl.java, val properties generate final fields on the JVM, which are inherently thread-safe for reference sharing and allow the JIT compiler to perform additional optimizations like inlining.

When should I use lateinit var instead of val in Android?

Use lateinit var when you must declare a property at class construction time but cannot initialize it until later in the Android lifecycle, such as in onCreate() or onCreateView(). This is common for View Binding, where the view hierarchy doesn't exist when the Fragment is instantiated. Note that lateinit requires var because the property must be reassigned once when initialized, and accessing it before initialization throws an UninitializedPropertyAccessException.

Is there a performance difference between val and var on Android?

Yes, there is a subtle performance advantage to val. Because val compiles to a final field on the JVM, the JIT compiler can inline the value at call sites and optimize memory layout more aggressively. var properties require generated getter and setter methods (unless private) and non-final fields, introducing slight method call overhead and preventing certain optimizations. For high-frequency operations on Android's main thread, preferring val can contribute to smoother frame rates.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →