Kotlin Setter and Getter

Understanding Kotlin Properties and Default Accessors

In Kotlin programming, properties are fundamental building blocks that simplify data encapsulation compared to traditional Java getters and setters. When you declare a Kotlin property, the compiler automatically generates default getter and setter methods behind the scenes.

Basic Property Declaration

Kotlin properties can be declared using two keywords:

  • var - Creates a mutable property with both getter and setter
  • val - Creates an immutable property with only a getter
class User {
    var name: String = "Default Name"    // Mutable property with getter and setter
    val userId: Int = 12345             // Immutable property with only getter
}

Here’s how Kotlin automatic getter setter generation works:

class Person {
    var age: Int = 25
        get() = field        // Default getter returns the backing field
        set(value) {         // Default setter assigns value to backing field
            field = value
        }
}

Accessing Properties Through Getters and Setters

Kotlin property access is seamless and doesn’t require explicit method calls:

fun main() {
    val person = Person()
    
    // Using implicit setter
    person.age = 30
    
    // Using implicit getter
    println("Person's age: ${person.age}")
}

Custom Getters in Kotlin

Custom getters in Kotlin allow you to add logic when retrieving property values. This is particularly useful for computed properties and derived values.

Creating Custom Getters

A custom getter is defined using the get() function after the property declaration:

class Rectangle {
    var width: Double = 0.0
    var height: Double = 0.0
    
    // Custom getter for computed property
    val area: Double
        get() = width * height
    
    // Custom getter with validation logic
    val isSquare: Boolean
        get() = width == height
}

Custom Getter with Transformation

Kotlin getter transformation allows you to modify returned values:

class UserProfile {
    var email: String = ""
    
    // Custom getter that always returns lowercase email
    val normalizedEmail: String
        get() = email.lowercase().trim()
    
    // Custom getter with conditional logic
    val displayName: String
        get() = if (email.isNotEmpty()) email.substringBefore("@") else "Anonymous"
}

Custom Setters in Kotlin

Custom setters in Kotlin enable you to add validation, transformation, or side effects when assigning values to properties.

Basic Custom Setter

A custom setter is defined using the set(value) function:

class BankAccount {
    var balance: Double = 0.0
        set(value) {
            if (value >= 0) {
                field = value
            } else {
                println("Balance cannot be negative!")
            }
        }
}

Custom Setter with Validation

Kotlin property validation through custom setters ensures data integrity:

class UserAccount {
    var password: String = ""
        set(value) {
            require(value.length >= 8) { "Password must be at least 8 characters" }
            require(value.any { it.isDigit() }) { "Password must contain a digit" }
            field = value
        }
    
    var age: Int = 0
        set(value) {
            require(value in 0..150) { "Age must be between 0 and 150" }
            field = value
        }
}

Custom Setter with Side Effects

Kotlin setter side effects can trigger additional operations:

class TemperatureMonitor {
    var temperature: Double = 20.0
        set(value) {
            field = value
            notifyObservers(value)
            logTemperatureChange(value)
        }
    
    private fun notifyObservers(temp: Double) {
        println("Temperature changed to: $temp°C")
    }
    
    private fun logTemperatureChange(temp: Double) {
        // Log to file or database
        println("Logging temperature: $temp")
    }
}

Understanding Backing Fields in Kotlin

Kotlin backing fields are essential for property storage and preventing infinite recursion in custom accessors.

The Field Identifier

The field identifier in Kotlin backing field refers to the actual storage location of the property:

class Counter {
    var count: Int = 0
        get() = field           // Returns the backing field value
        set(value) {
            if (value >= 0) {
                field = value   // Assigns to the backing field
            }
        }
}

When Backing Fields Are Generated

Kotlin backing field generation follows specific rules:

  1. Property uses default implementation of at least one accessor
  2. Custom accessor references the field identifier
class Example {
    // Backing field generated (uses default accessors)
    var property1: String = "value"
    
    // Backing field generated (custom accessor uses 'field')
    var property2: String = "value"
        set(value) {
            field = value.uppercase()
        }
    
    // No backing field generated (no default accessors, no 'field' reference)
    val property3: String
        get() = "computed value"
}

Avoiding Infinite Recursion

Kotlin recursive setter prevention is crucial for avoiding stack overflow errors:

class SafeProperty {
    var value: String = ""
        get() = field          // Correct: uses backing field
        set(newValue) {
            field = newValue   // Correct: assigns to backing field
        }
}

class UnsafeProperty {
    var value: String = ""
        get() = value          // Wrong: causes infinite recursion
        set(newValue) {
            value = newValue   // Wrong: causes infinite recursion
        }
}

Private Setters and Public Getters

Kotlin private setter allows you to create read-only properties from outside the class while maintaining write access internally.

Implementing Private Setters

class GameScore {
    var score: Int = 0
        private set    // Public getter, private setter
    
    var playerName: String = ""
        private set(value) {   // Custom private setter
            field = value.trim().takeIf { it.isNotEmpty() } ?: "Anonymous"
        }
    
    fun increaseScore(points: Int) {
        score += points
    }
    
    fun setPlayerName(name: String) {
        playerName = name
    }
}

Encapsulation with Private Setters

Kotlin encapsulation through private setters provides controlled access:

class DatabaseConnection {
    var isConnected: Boolean = false
        private set
    
    var connectionUrl: String = ""
        private set
    
    fun connect(url: String): Boolean {
        connectionUrl = url
        isConnected = establishConnection(url)
        return isConnected
    }
    
    fun disconnect() {
        isConnected = false
        connectionUrl = ""
    }
    
    private fun establishConnection(url: String): Boolean {
        // Connection logic here
        return true
    }
}

Advanced Kotlin Property Patterns

Lazy Properties with Custom Getters

Kotlin lazy properties combined with custom getters provide efficient initialization:

class DataProcessor {
    private var _data: String? = null
    
    val processedData: String
        get() {
            if (_data == null) {
                _data = loadAndProcessData()
            }
            return _data!!
        }
    
    private fun loadAndProcessData(): String {
        // Expensive data processing
        return "Processed data"
    }
}

Property Delegation

Kotlin property delegation offers advanced property management:

import kotlin.properties.Delegates

class ObservableProperties {
    var observedProperty: String by Delegates.observable("initial") { 
        property, oldValue, newValue ->
        println("Property ${property.name} changed from $oldValue to $newValue")
    }
    
    var vetoableProperty: Int by Delegates.vetoable(0) { 
        property, oldValue, newValue ->
        newValue >= 0  // Only allow non-negative values
    }
}

Backing Properties

Kotlin backing properties provide complete control over property access:

class SecureData {
    private var _sensitiveData: String? = null
    
    val sensitiveData: String?
        get() = _sensitiveData?.let { decrypt(it) }
    
    fun setSensitiveData(data: String) {
        _sensitiveData = encrypt(data)
    }
    
    private fun encrypt(data: String): String = "encrypted_$data"
    private fun decrypt(data: String): String = data.removePrefix("encrypted_")
}

Practical Android Development Examples

ViewModel with Custom Properties

import androidx.lifecycle.ViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData

class UserProfileViewModel : ViewModel() {
    private val _username = MutableLiveData<String>()
    val username: LiveData<String> = _username
    
    var userInput: String = ""
        set(value) {
            field = value.trim()
            _username.value = field
            validateInput(field)
        }
    
    var isValid: Boolean = false
        private set
    
    private fun validateInput(input: String) {
        isValid = input.length >= 3 && input.isNotBlank()
    }
}

Data Class with Validation

data class MobileUser(
    private var _phoneNumber: String,
    private var _email: String
) {
    var phoneNumber: String
        get() = _phoneNumber
        set(value) {
            require(isValidPhoneNumber(value)) { "Invalid phone number format" }
            _phoneNumber = value
        }
    
    var email: String
        get() = _email.lowercase()
        set(value) {
            require(isValidEmail(value)) { "Invalid email format" }
            _email = value
        }
    
    private fun isValidPhoneNumber(phone: String): Boolean {
        return phone.matches(Regex("^\\+?[1-9]\\d{1,14}\$"))
    }
    
    private fun isValidEmail(email: String): Boolean {
        return email.contains("@") && email.contains(".")
    }
    
    init {
        phoneNumber = _phoneNumber
        email = _email
    }
}

Complete Working Example

Here’s a comprehensive example demonstrating Kotlin getter setter concepts in a real-world scenario:

import kotlin.math.max
import kotlin.math.min

/**
 * Mobile App User Profile with comprehensive property management
 * Demonstrates Kotlin getters, setters, backing fields, and validation
 */
class MobileAppUser(initialEmail: String) {
    
    // Property with custom setter validation
    var email: String = ""
        set(value) {
            require(value.isNotEmpty()) { "Email cannot be empty" }
            require(value.contains("@")) { "Email must contain @ symbol" }
            field = value.lowercase().trim()
            println("Email updated to: $field")
        }
        get() = field
    
    // Property with private setter and validation
    var profileScore: Int = 0
        private set(value) {
            field = max(0, min(100, value))  // Clamp between 0 and 100
        }
    
    // Computed property with custom getter
    val emailDomain: String
        get() = email.substringAfter("@")
    
    // Property with custom getter and backing field
    var _displayName: String = ""
    var displayName: String
        get() = if (_displayName.isNotEmpty()) _displayName else email.substringBefore("@")
        set(value) {
            _displayName = value.trim().take(50)  // Limit to 50 characters
        }
    
    // Property with side effects in setter
    var isActive: Boolean = true
        set(value) {
            if (field != value) {  // Only act on actual changes
                field = value
                onActiveStatusChanged(value)
            }
        }
    
    // Read-only computed property
    val userSummary: String
        get() = "User: $displayName ($email) - Score: $profileScore - Active: $isActive"
    
    init {
        email = initialEmail
        updateProfileScore(25)  // Initial score
    }
    
    // Public method to update private setter property
    fun updateProfileScore(newScore: Int) {
        profileScore = newScore
    }
    
    fun increaseScore(points: Int) {
        profileScore += points
    }
    
    fun deactivateUser() {
        isActive = false
    }
    
    private fun onActiveStatusChanged(active: Boolean) {
        println("User ${displayName} is now ${if (active) "active" else "inactive"}")
        // Could trigger analytics, notifications, etc.
    }
}

// Usage example
fun main() {
    // Create user with validation
    val user = MobileAppUser("john.doe@example.com")
    
    // Property assignments trigger custom setters
    user.displayName = "John Doe"
    user.increaseScore(25)
    
    // Access computed properties
    println("Email domain: ${user.emailDomain}")
    println("User summary: ${user.userSummary}")
    
    // Trigger side effects
    user.deactivateUser()
    
    // Attempt invalid email (will throw exception)
    try {
        user.email = "invalid-email"
    } catch (e: IllegalArgumentException) {
        println("Validation error: ${e.message}")
    }
}

Output:

Email updated to: john.doe@example.com
Email domain: example.com
User summary: User: John Doe (john.doe@example.com) - Score: 50 - Active: true
User John Doe is now inactive
Validation error: Email must contain @ symbol

This example showcases all major Kotlin property concepts including custom getters, custom setters, backing fields, property validation, private setters, computed properties, and side effects in a practical Android development context.