Popular Searches
Popular Course Categories
Popular Courses

Understanding Properties, Getters, Setters, and Visibility Modifiers

Understanding Properties, Getters, Setters, and Visibility Modifiers

Object-Oriented Kotlin and Advanced Language Features

Understanding Properties, Getters, Setters, and Visibility Modifiers in Kotlin

Course: Android App Development

Topic: Understanding Properties, Getters, Setters, and Visibility Modifiers

Course Link: Android App Development Course

Demo Link: Register for a Course Demo

Website: JustAcademy


1. Introduction

Properties, getters, setters, and visibility modifiers are essential concepts in Kotlin programming and Android application development. They help developers store information, control how values are accessed or modified, validate user input, and protect sensitive application data.

Kotlin uses properties to represent data associated with classes, objects, and top-level declarations. Unlike traditional Java code, where developers commonly write separate getter and setter methods, Kotlin provides built-in accessors for properties. Developers can also customize these accessors when additional logic is required.

Visibility modifiers determine where classes, properties, functions, constructors, and setters can be accessed. Using the correct visibility level helps create secure, maintainable, and well-organized applications.

Learning Objectives

  • Understand Kotlin properties and their purpose.
  • Differentiate between val and var.
  • Understand default getters and setters.
  • Create custom getters and setters.
  • Use backing fields with the field keyword.
  • Apply validation logic inside setters.
  • Understand Kotlin visibility modifiers.
  • Restrict property modification using private setters.
  • Apply encapsulation in Android application models.
  • Identify common errors related to properties and access control.

2. What Are Properties in Kotlin?

A property represents a value associated with a class, object, interface, or top-level declaration. It provides a convenient way to read or modify data without manually defining separate accessor methods for every variable.

A property generally consists of a name, a type, and accessors. A property declared with val is read-only after initialization, while a property declared with var can be reassigned.

Example: Declaring Properties

class Student {
    val studentId: Int = 101
    var studentName: String = "Aarav"
    var marks: Int = 85
}

fun main() {
    val student = Student()

    println(student.studentId)
    println(student.studentName)
    println(student.marks)

    student.studentName = "Priya"
    student.marks = 92

    println(student.studentName)
    println(student.marks)
}

Output:

101
Aarav
85
Priya
92

In this example, studentId is read-only, whereas studentName and marks can be updated after the object is created.

3. Difference Between val and var

Kotlin provides two primary property declaration keywords: val and var. Choosing the correct keyword communicates whether reassignment is allowed.

Feature val var
Meaning Read-only property Mutable property
Reassignment Not allowed after initialization Allowed when the type permits it
Default setter No setter Has a setter
Custom getter Allowed Allowed
Custom setter Not allowed Allowed
Example val appName = "MyApp" var score = 90

Example of val

fun main() {
    val courseName = "Android Development"
    println(courseName)

    // This would cause a compilation error:
    // courseName = "Kotlin Development"
}

A val prevents reassignment of the property reference. However, if a val refers to a mutable object, the object's internal state may still be changed.

Example of var

fun main() {
    var currentScore = 50

    println(currentScore)

    currentScore = 75

    println(currentScore)
}

Output:

50
75

4. Properties in Primary Constructors

Kotlin allows properties to be declared directly in a class's primary constructor by using val or var. This reduces boilerplate code and makes class definitions more concise.

Example

class Employee(
    val employeeId: Int,
    var employeeName: String,
    var salary: Double
)

fun main() {
    val employee = Employee(101, "Rohan", 45000.0)

    println(employee.employeeId)
    println(employee.employeeName)
    println(employee.salary)

    employee.employeeName = "Rahul"
    employee.salary = 50000.0

    println(employee.employeeName)
    println(employee.salary)
}

Explanation:

  • employeeId is declared with val, so it cannot be reassigned through the property.
  • employeeName and salary are declared with var, so they can be updated.
  • Kotlin automatically provides the appropriate accessors.
  • Constructor parameters initialize the properties when the object is created.

5. Understanding Getters

A getter is an accessor that provides a property's value when the property is read. Kotlin generates a default getter for properties automatically, so developers usually access the property directly by its name.

For example, when the code reads student.studentName, Kotlin invokes the property's getter. Developers do not normally need to call a method such as getStudentName() in Kotlin source code.

Example of a Default Getter

class Product {
    val productName: String = "Laptop"
}

fun main() {
    val product = Product()
    println(product.productName)
}

Output:

Laptop

Custom Getter

A custom getter lets developers calculate or transform a property's value whenever it is accessed. It is useful when the value can be derived from other properties or when a calculated result should be exposed as a property.

class Rectangle(
    val width: Int,
    val height: Int
) {
    val area: Int
        get() = width * height
}

fun main() {
    val rectangle = Rectangle(10, 5)

    println("Width: ${rectangle.width}")
    println("Height: ${rectangle.height}")
    println("Area: ${rectangle.area}")
}

Output:

Width: 10
Height: 5
Area: 50

The area property does not need to store a separate value. Its getter calculates the result whenever the property is accessed.

Custom Getter for Formatting

class User(
    val firstName: String,
    val lastName: String
) {
    val fullName: String
        get() = "$firstName $lastName"
}

fun main() {
    val user = User("Aarav", "Sharma")
    println(user.fullName)
}

Output:

Aarav Sharma

A computed getter is useful for values such as full names, calculated totals, display labels, and derived status information.

6. Understanding Setters

A setter is an accessor that controls how a mutable property is updated. Kotlin automatically generates a default setter for a var property. Developers can create a custom setter to validate, transform, or react to a new value.

A setter receives the new value through a parameter commonly named value.

Example of a Default Setter

class Student {
    var marks: Int = 0
}

fun main() {
    val student = Student()

    student.marks = 80
    println(student.marks)

    student.marks = 95
    println(student.marks)
}

Output:

80
95

When a new value is assigned to marks, Kotlin invokes the default setter to update the property's stored value.

Custom Setter for Validation

A custom setter can reject invalid input or normalize values before storing them. This is useful when a property must follow a business rule.

class Student {
    var marks: Int = 0
        set(value) {
            if (value in 0..100) {
                field = value
            } else {
                println("Marks must be between 0 and 100")
            }
        }
}

fun main() {
    val student = Student()

    student.marks = 85
    println(student.marks)

    student.marks = 120
    println(student.marks)
}

Output:

85
Marks must be between 0 and 100
85

The setter checks whether the new value is within the valid range. If it is valid, the setter stores it using field = value. If it is invalid, the existing value remains unchanged.

7. Understanding the field Keyword and Backing Fields

The field keyword refers to the property's backing field inside a custom getter or setter. A backing field is the storage location used to hold a property's value when the compiler needs one.

Without a backing field, a property can still work as a computed property if its getter derives the value from other data.

Example of a Backing Field

class Temperature {
    var celsius: Double = 0.0
        set(value) {
            if (value >= -273.15) {
                field = value
            } else {
                println("Temperature cannot be below absolute zero")
            }
        }
}

fun main() {
    val temperature = Temperature()

    temperature.celsius = 25.0
    println(temperature.celsius)

    temperature.celsius = -300.0
    println(temperature.celsius)
}

Output:

25.0
Temperature cannot be below absolute zero
25.0

Why Use field?

  • It accesses the property's stored value inside an accessor.
  • It avoids recursively calling the same setter or getter.
  • It allows validation before updating a property.
  • It supports logging or other actions when values change.

Avoiding Infinite Recursion

Inside a custom setter, assigning a value to the property itself invokes the setter again. This can cause recursive calls. Use field = value to update the backing field instead.

class Counter {
    var count: Int = 0
        set(value) {
            field = value
        }
}

Here, field = value updates the stored value directly rather than invoking the setter repeatedly.

8. Combining Custom Getters and Setters

A mutable property can have both a custom getter and a custom setter. This allows a class to control how values are stored, validated, and presented.

Example: Product Price

class Product {
    var price: Double = 0.0
        set(value) {
            if (value >= 0) {
                field = value
            } else {
                println("Price cannot be negative")
            }
        }
        get() = field
}

fun main() {
    val product = Product()

    product.price = 1500.0
    println(product.price)

    product.price = -200.0
    println(product.price)
}

Output:

1500.0
Price cannot be negative
1500.0

The custom setter validates the assigned price, while the custom getter returns the stored value. Because the getter only returns the backing field, the explicit getter is not strictly necessary in this example; Kotlin's default getter would work equally well.

9. Property Accessor Visibility

Kotlin allows developers to restrict access to a property's setter while keeping its getter available. This pattern is useful when external code should be able to read a value but should not be able to change it directly.

Example: Private Setter

class BankAccount(initialBalance: Double) {
    var balance: Double = initialBalance
        private set

    fun deposit(amount: Double) {
        if (amount > 0) {
            balance += amount
        }
    }

    fun withdraw(amount: Double) {
        if (amount > 0 && amount <= balance) {
            balance -= amount
        }
    }
}

fun main() {
    val account = BankAccount(1000.0)

    account.deposit(500.0)
    account.withdraw(200.0)

    println(account.balance)
}

Output:

1300.0

The balance property can be read from outside the class, but its setter is private. Only code with the appropriate access inside the declaring class can assign to it directly. Public methods such as deposit() and withdraw() enforce the account's rules.

Why Use a Private Setter?

  • Prevents arbitrary external changes to important data.
  • Ensures updates pass through controlled methods.
  • Supports encapsulation and validation.
  • Reduces the risk of invalid application state.

10. Understanding Visibility Modifiers

Visibility modifiers define where a declaration can be accessed. Kotlin provides four main visibility modifiers: public, private, protected, and internal.

If no visibility modifier is specified, Kotlin uses public by default.

Modifier Meaning Typical Use
public Accessible wherever the declaring type or member is accessible. Public APIs and commonly used functions.
private Restricts access to the declaring scope. Internal implementation details and sensitive state.
protected For class members, accessible in the declaring class and subclasses. Members intended for use by subclasses.
internal Accessible within the same Kotlin module. Features shared across a module but not exposed as a public API.

11. Public Visibility Modifier

The public modifier allows access wherever the declaration itself is visible. Because public is the default, it can be omitted when public access is intended.

Example

class UserProfile {
    public val username: String = "aarav123"

    public fun displayUsername() {
        println(username)
    }
}

fun main() {
    val profile = UserProfile()

    println(profile.username)
    profile.displayUsername()
}

Output:

aarav123
aarav123

Both the property and function are public, so they can be accessed from code that can access the UserProfile class.

12. Private Visibility Modifier

The private modifier restricts access to a declaration. For a member inside a class, private visibility means that the member can be accessed within that class. For a top-level declaration, private visibility limits access to the containing Kotlin file.

Example

class LoginManager {
    private val password: String = "secure123"

    fun validatePassword(input: String): Boolean {
        return input == password
    }
}

fun main() {
    val manager = LoginManager()

    println(manager.validatePassword("secure123"))

    // This would cause a compilation error:
    // println(manager.password)
}

Output:

true

The password is hidden from direct access outside the class. The public validation function provides a controlled way to use the private property.

Security note: This example demonstrates visibility only. Hard-coded passwords should not be used in real applications, and private visibility is not a substitute for secure credential storage.

13. Protected Visibility Modifier

The protected modifier is used for class members that should be accessible within the declaring class and its subclasses, but not through ordinary access from unrelated code. It is not available for top-level declarations.

Example

open class Vehicle {
    protected val engineType: String = "Electric"

    protected fun startEngine() {
        println("Engine started")
    }
}

class ElectricCar : Vehicle() {
    fun displayDetails() {
        println(engineType)
        startEngine()
    }
}

fun main() {
    val car = ElectricCar()
    car.displayDetails()

    // Direct external access is not allowed:
    // println(car.engineType)
}

Output:

Electric
Engine started

The ElectricCar subclass can access the protected property and function inherited from Vehicle. Unrelated code cannot access them directly through the object.

14. Internal Visibility Modifier

The internal modifier makes a declaration accessible within the same Kotlin module. A module is a set of Kotlin files compiled together, such as a Gradle module or a compilation unit.

Example

internal class AppConfiguration {
    internal val appVersion: String = "1.0.0"

    internal fun displayVersion() {
        println(appVersion)
    }
}

fun main() {
    val config = AppConfiguration()
    config.displayVersion()
}

Output:

1.0.0

Code in the same module can access these declarations. Code in a separate module generally cannot access them as ordinary public API. The exact module boundaries depend on the project's build configuration.

15. Comparing Visibility Modifiers

Access Location public private Class Member protected Class Member internal
Inside declaring class Yes Yes Yes Yes, within the module
Subclass Yes, if accessible No direct inherited access Yes Yes, within the module
Unrelated class in same module Yes, if accessible No No direct access Yes
Other module Yes, if the declaration is exposed and accessible No No direct access No ordinary access
Top-level declaration Allowed Allowed, limited to file Not allowed Allowed

Note: Visibility is also affected by the visibility of the containing class, package imports, module boundaries, and the exact declaration scope.

16. Applying Visibility Modifiers to Properties and Setters

A property and its setter can have different visibility. The getter always has the same visibility as the property itself, while the setter can be made less visible.

Example: Public Read Access and Private Write Access

class UserAccount {
    var username: String = "guest"
        private set

    fun updateUsername(newUsername: String) {
        if (newUsername.isNotBlank()) {
            username = newUsername
        }
    }
}

fun main() {
    val account = UserAccount()

    println(account.username)

    account.updateUsername("priya")
    println(account.username)

    // This would cause a compilation error:
    // account.username = "admin"
}

Output:

guest
priya

The public property getter allows the username to be read. The private setter prevents external code from assigning a value directly. The public method applies validation before updating the property.

17. Properties and Encapsulation

Encapsulation is the practice of keeping an object's internal state protected and exposing controlled operations to other parts of the application. Properties, accessors, and visibility modifiers work together to implement encapsulation in Kotlin.

Example: Inventory Management

class InventoryItem(
    val itemName: String,
    initialQuantity: Int
) {
    var quantity: Int = initialQuantity
        private set

    fun addStock(amount: Int) {
        if (amount > 0) {
            quantity += amount
        }
    }

    fun removeStock(amount: Int): Boolean {
        if (amount > 0 && amount <= quantity) {
            quantity -= amount
            return true
        }

        return false
    }
}

fun main() {
    val item = InventoryItem("Keyboard", 10)

    item.addStock(5)
    println(item.quantity)

    item.removeStock(3)
    println(item.quantity)
}

Output:

15
12

The inventory quantity can be read from outside the class but cannot be changed directly. The methods ensure that stock is only added or removed according to the defined rules.

Benefits of Encapsulation

  • Protects application state from invalid changes.
  • Centralizes validation logic.
  • Makes debugging easier by controlling where changes occur.
  • Allows implementation details to change without requiring changes to callers.
  • Improves maintainability and testability.

18. Properties in Android Application Development

Properties are used throughout Android applications to represent user data, screen state, configuration values, application settings, and model information. Appropriate access control helps prevent accidental changes to important state.

Example: Profile Information

data class Profile(
    val userId: Int,
    var displayName: String,
    val email: String
)

fun main() {
    val profile = Profile(
        userId = 101,
        displayName = "Aarav",
        email = "[email protected]"
    )

    println(profile.displayName)

    profile.displayName = "Aarav Sharma"

    println(profile.displayName)
}

Output:

Aarav
Aarav Sharma

In this model, the user ID and email are read-only through their properties, while the display name can be updated. Whether email should be mutable depends on the application's requirements.

Example: Managing Screen State

class LoginState {
    var isLoading: Boolean = false
        private set

    var errorMessage: String? = null
        private set

    fun startLoading() {
        isLoading = true
        errorMessage = null
    }

    fun showError(message: String) {
        isLoading = false
        errorMessage = message
    }

    fun finishLoading() {
        isLoading = false
    }
}

This example encapsulates the loading state and error message. Only the class's methods can modify these properties directly, while code with an instance can read their current values.

For a real Android project, observable state holders such as StateFlow or LiveData may be appropriate when UI components need to react automatically to state changes.

19. Common Mistakes and Troubleshooting

Problem Cause Solution
Reassigning a val property val is read-only after initialization. Use var if reassignment is genuinely required.
Declaring a setter for val Read-only properties cannot have setters. Change the declaration to var if a setter is needed.
Recursive setter calls The setter assigns to the property itself. Use field = value to update the backing field.
Unexpected computed value A custom getter calculates the result on every access. Check the calculation and the source properties.
Cannot access private property The code is outside the permitted scope. Expose a safe public method or a readable property if appropriate.
Cannot access protected member The code is not in the declaring class or a permitted subclass context. Access it from the class hierarchy or redesign the public API.
Internal declaration unavailable The caller is in another module. Review module boundaries and choose the intended visibility.
External code changes important data The property setter is publicly accessible. Use a private setter and controlled update methods.
Invalid data is stored The setter does not validate incoming values. Add validation or expose operations that enforce the required rules.

20. Best Practices

  • Prefer val when reassignment is unnecessary.
  • Use var only for values that must change.
  • Use custom getters for calculated or formatted values.
  • Use custom setters when property updates require validation or side effects.
  • Use field inside accessors when direct access to the backing field is required.
  • Avoid unnecessary custom accessors when the default behavior is sufficient.
  • Use private setters for state that should only be changed through class methods.
  • Use private for implementation details that should not be exposed.
  • Use protected for members intended for subclasses.
  • Use internal for APIs shared within a module.
  • Keep public APIs small and clear.
  • Do not rely on visibility modifiers alone to protect secrets or sensitive information.
  • Write unit tests for setter validation and property-derived calculations.
  • Document unusual property behavior so that other developers understand the rules.

21. Practical Project: Student Grade Management

Objective: Create a student class that uses properties, a custom setter, a computed getter, and a private setter to manage student information safely.

Project Requirements

  • Store the student's name as a mutable property.
  • Accept marks only between 0 and 100.
  • Calculate a grade using a custom getter.
  • Keep the marks property publicly readable but privately writable.
  • Provide a public method to update marks after validation.

Sample Implementation

class StudentGrade(
    val studentId: Int,
    var studentName: String,
    initialMarks: Int
) {
    var marks: Int = initialMarks
        private set

    val grade: String
        get() = when (marks) {
            in 90..100 -> "A"
            in 75..89 -> "B"
            in 60..74 -> "C"
            in 40..59 -> "D"
            else -> "F"
        }

    fun updateMarks(newMarks: Int): Boolean {
        if (newMarks !in 0..100) {
            return false
        }

        marks = newMarks
        return true
    }
}

fun main() {
    val student = StudentGrade(
        studentId = 101,
        studentName = "Priya",
        initialMarks = 82
    )

    println("Name: ${student.studentName}")
    println("Marks: ${student.marks}")
    println("Grade: ${student.grade}")

    student.updateMarks(95)

    println("Updated Marks: ${student.marks}")
    println("Updated Grade: ${student.grade}")
}

Expected Output:

Name: Priya
Marks: 82
Grade: B
Updated Marks: 95
Updated Grade: A

Learning outcome: This project demonstrates read-only properties, mutable properties, a computed getter, a private setter, and controlled data updates through a public function.

Additional tasks:

  1. Reject invalid initial marks in the constructor or through a factory function.
  2. Add a computed property that returns whether the student has passed.
  3. Make the student ID private if it should only be exposed through a controlled function.
  4. Write tests for valid marks, invalid marks, and grade boundaries.

22. Interview Questions and Answers

Q1. What is a property in Kotlin?

Answer: A property represents data associated with a class, object, interface, or top-level declaration. Kotlin provides accessors for reading and, when applicable, modifying it.

Q2. What is the difference between val and var?

Answer: val is read-only after initialization, while var allows reassignment.

Q3. What is a getter?

Answer: A getter is an accessor that supplies a property's value when it is read.

Q4. What is a setter?

Answer: A setter is an accessor that controls how a mutable property's value is updated.

Q5. Can a val property have a custom setter?

Answer: No. A val property is read-only and cannot have a setter.

Q6. What is a custom getter?

Answer: A custom getter defines how a property's value is calculated or returned whenever the property is accessed.

Q7. What is the purpose of the field keyword?

Answer: The field keyword accesses the backing field from within a property's custom getter or setter.

Q8. What is a backing field?

Answer: A backing field is the storage used by a property when the compiler needs to retain its value.

Q9. What is the default visibility in Kotlin?

Answer: The default visibility modifier is public.

Q10. What is the difference between private and protected?

Answer: A private class member is accessible within its declaring class, while a protected class member is also accessible to subclasses.

Q11. What does internal mean?

Answer: It means that a declaration is accessible within the same Kotlin module.

Q12. Can a property have a private setter and a public getter?

Answer: Yes. For example, var balance: Double = 0.0 followed by private set permits external reading but restricts direct external assignment.

Q13. What is encapsulation?

Answer: Encapsulation protects internal state and exposes controlled operations for interacting with an object.

Q14. When should a custom setter be used?

Answer: Use a custom setter when values need validation, normalization, logging, or other actions during assignment.

Q15. How are Kotlin properties represented in Java?

Answer: Kotlin properties commonly compile to getter methods and, for mutable properties, setter methods. A backing field may also be generated when required.

23. Quick Revision Checklist

  • Understand Kotlin properties and their purpose.
  • Differentiate between val and var.
  • Understand default getters and setters.
  • Write a custom getter for a calculated property.
  • Write a custom setter for validation.
  • Use the field keyword correctly.
  • Understand public, private, protected, and internal visibility.
  • Restrict write access using private set.
  • Apply encapsulation to application state.
  • Test property validation and access restrictions.

24. Additional Learning Resources

25. Conclusion

Properties, getters, setters, and visibility modifiers form an important foundation for writing structured Kotlin applications. Properties provide a convenient way to represent data, getters control how values are read, and setters control how mutable values are updated. Visibility modifiers determine which parts of a program can access those declarations.

By using custom accessors when necessary, validating values, and restricting access to important state, Android developers can create safer and more maintainable applications. Practice these concepts through student records, inventory management, user profiles, and application state models to develop a strong understanding of Kotlin encapsulation.

Continue Learning: Android App Development Course | Register for a Course Demo | JustAcademy

Previous Next
whatsapp