1. Introduction

In this tutorial, we’ll learn how to define a variable with a public getter and a private setter in Kotlin. That allows us to have read-only access to the variable from outside the class while also restricting write access to only the inside class itself.

To know more about visibility modifiers in Kotlin, refer to our article: Getters and Setters in Kotlin.

2. Defining a Private Setter

When we define a new variable in Kotlin, the default modifier is public: The value can be modified from both inside and outside the class.

If we want to forbid write access outside the class, we can define the variable with a private setter:

class MyClass {
    var myProperty: String = "default"
        private set
}

Now, if we try to change the value of the property from another class:

val obj = MyClass()
obj.myProperty = "new value" // compilation error

The compiler will raise the error:

Cannot assign to 'myProperty': the setter is private in 'MyClass'

3. How Private Setters Can Be Useful

In this section, we’ll explore a few examples of how private setters can be useful in more realistic examples.

3.1. Maintain Class Constraints with Private Setter

Let’s take, as an example, a banking application where we model the user’s bank account. We want to allow users to read their balances, make deposits, and take withdrawals.

But if we allow the user to update the account balance directly, it’s possible that the bank account will end up in an invalid state. For instance, the user could withdraw more than what he actually owns.

We can avoid those problems by defining a private setter on the balance, making it read-only from outside the class, and providing methods to deposit and withdraw funds:

class BankAccount {
    var balance: Int = 0
        private set

    fun deposit(amount: Int) {
        require(amount > 0) { "Deposit Amount cannot be negative or zero" }
        balance += amount
    }

    fun withdraw(amount: Int) {
        require(amount > 0) { "Withdraw Amount cannot be negative or zero" }
        require(amount <= balance) { "Withdraw Amount cannot be greater than balance" }
        balance -= amount
    }
}

Let’s see our BankAccount class in action:

val bankAccount = BankAccount()
bankAccount.deposit(100)
bankAccount.withdraw(50)
println("Bank Account balance is: " + bankAccount.balance)
// Output: Bank Account balance is: 50

In the example, the consumers of the class BankAccount can now update the balance as they wish using the deposit() and withdraw() methods, ensuring that the balance remains consistent and in a valid state.

For instance, if we try to withdraw more than the account has, the require() check will throw an exception:

val bankAccount = BankAccount()
bankAccount.deposit(100)
bankAccount.withdraw(1000)
// Throws: IllegalArgumentException: Withdraw Amount cannot be greater than balance

We can’t bypass the preconditions check in the methods. If we try to update the account balance directly:

bankAccount.balance -= 1000

The compiler will raise the expected error:

Cannot assign to 'balance': the setter is private in 'BankAccount'

3.2. Custom Private Setter

Using a private setter allows us to encapsulate and control access to a property. Sometimes, we may need more control over how a variable is set beyond restricting access. In other cases, we may want to execute specific logic every time a variable is set.

In such a scenario, using custom private setters could be helpful. Continuing from the BankAccount example, we may want to keep a history log of every time the account balance changes:

class LoggingBankAccount {
    var balance: Int = 0
        private set(value) {
            println("Balance changed from $field to $value")
            field = value
        }

    fun deposit(amount: Int) {
        require(amount > 0) { "Deposit Amount cannot be negative or zero" }
        balance += amount
    }

    fun withdraw(amount: Int) {
        require(amount > 0) { "Withdraw Amount cannot be negative or zero" }
        require(amount <= balance) { "Withdraw Amount cannot be greater than balance" }
        balance -= amount
    }
}

Let’s see what happens when we invoke our deposit and withdraw methods from the LoggingBankAccount class:

val bankAccount = LoggingBankAccount()
bankAccount.deposit(100)
bankAccount.withdraw(50)
println("Bank Account balance is: " + bankAccount.balance)

Every time we update the balance, we’ll log the values before and after the update:

Balance changed from 0 to 100
Balance changed from 100 to 50
Bank Account balance is: 50

4. Conclusion

In summary, private setters and public getters can be useful in scenarios where we need to provide read-only access to certain properties while restricting write access to only specific methods or classes.

In this article, we explored:

  • How to define a read-only setter to restrict write access to a variable outside its class
  • How to execute custom code every time a read-only setter is invoked

As always, the code for these examples is available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.