Instruction

Properties

In the previous lesson, you were briefly introduced to properties as a member of Kotlin classes. They’re often used to describe the object’s attributes or hold state. Look again at the following example:

class Food(val name: String, val price: String)

In this example, the Food class has two properties, name and price. Both are immutable and store values of type String. These properties are defined in the primary constructor and store different string values for each instance of the Food class.

Properties in Kotlin can go beyond simple data storage. You can define custom accessors, also called getters and setters, to control how properties are retrieved and modified. By default, properties defined in the primary constructor use built-in accessors that store the data in a hidden field. But what if you want more control?

In this lesson, you’ll learn much more about properties. You’ll learn about mutability, property initializers, custom accessors, delegated properties, late initialization, and extension properties.

Note: For all the coding examples, you’ll use Kotlin Playground as linked below: Kotlin Playground

Constructor Properties

Although you’ve done this many times already, now is a good time to dig slightly deeper into the definition of a class.

class Food(var name: String, var price: String)

Did you notice that this definition looks similar to how you define functions with arguments? Indeed, this is a definition of a special function that both defines properties and initializes them when called. This function is called, unsurprisingly, constructor.

You can use this class repeatedly to build an array of foods, each with a different value. The properties you want to store are a food name and price. Everything has a price!

price name Food

These are the properties of the Food class. You provide a data type for each but opt not to assign a default value because you plan to assign the name and price upon initialization. After all, the values will differ for each instance of Food.

Now, the real action begins. Open a fresh Kotlin Playground page and add a class definition above the main function. Inside the main function, create an instance of the Food class. For clarity, you’ll use property names to assign values. It’ll look like this:

class Food(var name: String, var price: String)

fun main() {
  // with named arguments
  val oneTomato = Food(
    name = "Tomato",
    price = "2.0"
  )
}

You created an instance of the Food class by passing values as arguments into the class’s primary constructor.

As with any function call that doesn’t use default values, using named arguments in the primary constructor is optional. Because named arguments are optional, you could also create the class instance like this:

// without named arguments
val twoTomato = Food("3.0", "Tomato")

Did you assign the properties correctly? Check by adding the following code into the main function:

println(twoTomato.name) // "3.0"
println(twoTomato.price) // "Tomato"

It seems this food was named as “3.0” and priced as “Tomato”. You just witnessed why named properties are more reliable. In the first scenario, you didn’t run the code to verify that the correct value was assigned to the correct property, especially when they’re the same type. As a bonus, the order for named properties doesn’t matter. You can always see which value was assigned to which property.

Mutability and Immutability

Take a look at the statement var name: String. This is how you define variables in functions and define class level variables or properties.

You may already know from other modules that you can control access to properties using the val and var keywords, but in case you need a refresher:

val: States that the property isn’t expected to change once set. In other words, it makes the property immutable.

var: States that the property is expected to change once set. In other words, it makes the property mutable.

Note: It’s easy to memorize which one is which by knowing that var is short for variable, a synonym for changeable. So the other one, val, isn’t changeable.

When designing classes for your apps, it’s good practice to assume that by default, the properties are immutable and use val. This prevents you or someone using your class from accidentally modifying a property value, a common reason for undiscovered bugs and one of the reasons these keywords were introduced in Kotlin.

Now, if you used this acquired knowledge to review the code examples from Lesson 1, you’d see that these good practices were used. But to showcase different scenarios, you’ll deviate from those practices here. “Do as I say, not as I do.”

Trust, but verify! Confirm that var properties can change. Continue in the main function:

  oneTomato.name = "Fresh Tomato"
  println(oneTomato.name) // "Fresh Tomato"

Did the real action just begin? For that, you need real food! Add this definition above the main function:

class RealFood(val name: String, var price: String)

Why is this real food, but the other one wasn’t? It’s the price that changes. The name of the food (almost) never changes.

Now you’ll see what would happen if you try to modify the val property. Replace the code in the main function with:

fun main() {
  // create RealFood
  val realTomato = RealFood("Tomato", "2.5")
  // Error: Val cannot be reassigned
  realTomato.name = "Real Tomato"
}

Note: Your experiments could stop the code compilation in Kotlin Playground. Once you verified that something isn’t working, comment the offending line where you try to assign to a val property above and continue with the experiments.

Access Modifiers

You’ve seen how to define properties in a class and how to control their mutability. Now, let’s talk about access modifiers. These are keywords that control the visibility of properties and methods in a class.

By default, properties are public, which means they can be accessed from anywhere in the code. But what if you want to restrict access to a property? You can use access modifiers to do this.

There are four access modifiers in Kotlin:

  • public: The default modifier. It allows access from anywhere in the code.
  • private: Restricts access to the class that contains the property.
  • protected: Allows access from the class that contains the property and its subclasses.
  • internal: Allows access from the same module.

Let’s see how access modifiers work in practice. Add this class definition above the main function:

class SecretFood {
  private val name
    get() = "Secret Tomato"
  val price = "3.0"
}

In this class, the name property is private, while the price property is public. This means you can access the price property from anywhere in the code, but you can only access the name property from within the SecretFood class.

Default Values

If you can reasonably assume what the value of a property should be when the type is initialized, you can give that property a default value. It doesn’t make sense to create a default name or price for a food, but imagine there’s a new property type to indicate what kind of food it is. Create a food class that has a default value:

class BetterFood(
  val name: String,
  var price: String,
  var kind: String = "Vegetable"
)

By assigning a value in the definition of type, you give this property a default value. Any food created will automatically be a vegetable (because why not?), unless you change the value of type to something like “Fruit” or “Bakery”:

fun main() {
  // create BetterFood
  val betterFood = BetterFood("Tomato", "4.0")
}

This betterFood is a “vegetable”. You could change that by reassigning the type:

  // reassign kind
  betterFood.kind = "Fruit"

Or providing the kind when you initially create it. Continue in the main function:

  // create BetterFood which is Fruit
  val betterFood2 = BetterFood("Tomato", "4.0", "Fruit")

Custom Accessors

Many properties work fine with the default accessor implementation, in which dot notation returns the value directly, and an assignment statement sets the value. Properties can also be defined with custom getter and setter methods. If a custom setter is provided, the property must be declared a var.

Custom Getter

A good example of a custom accessor is a food label printed to display in the store.

Add this class:

class Food(
  val name: String,
  var price: String,
  var origin: String) {

  // 1
  val label: String
    get() {
      // 2
      val result = if (origin == "US") {
        "Local $name. Price: \$$price"
      } else {
        "$origin $name. Price: $price"
      }
      // 3
      return result
    }
}

In the code above:

  1. You create a label to print and put on a display for the food. Instead of the usual assignment operator = to assign a value as you would for a normal property, you use the get() function and curly braces to enclose your property’s calculation.
  2. Since you know the food’s name, price, and origin, you can add some logic to say that if it’s from the US, then it’s local, and the price is likely in USD. Otherwise, you just show all the information available in the class.
  3. You return the result as a newly created string.

Note: The return type for the property is unrelated to the types of the properties used. For example, a new property could convert the price into an Int or Float value for easier calculations.

Since you provided a custom getter, no value is stored for label. It’s simply returned based on a calculation. From outside the class, a property with a custom getter can be accessed like any other property. Add this to your main function:

fun main() {
  val tomato = Food("Tomato", "2.0", "US")
  println(tomato.label) // Local Tomato. Price: $2.0
  tomato.origin = "UK"
  println(tomato.label) // UK Tomato. Price: 2.0
}

Custom Setter

The property you wrote in the previous section is called a read-only property. It has a block of code to compute the value of the property: the custom getter.

It’s also possible to create a read-write property with two blocks of code: a custom getter and a custom setter. This setter works differently than you might expect. Since the property has no place to store a value, the setter usually indirectly sets one or more related properties.

Add new property country to the Food class:

// custom setter
//1
var country: String
  // 2
  get() = "Country of origin: $origin"
  // 3
  set(value) {
    origin = value
  }

In this code:

  1. You created country to be a var instead of a val since you’re giving it a setter to change the value.
  2. You use some string wrapping to return a human-readable text.
  3. The setter assumes that country and origin are effectively the same.

In addition to setting the origin property during class creation, you can set it indirectly by setting the country property.

Notice that there’s no return statement in a setter — it only modifies the other stored properties. With the setter in place, you can provide a country of origin for the food. Try it out using the following code in the main function:

  tomato.country = "Chile"
  println(tomato.label) // Chile Tomato. Price: 2.0
  println(tomato.country) // Country of origin: Chile

Companion Object Properties

You’ve explored how properties within a class are unique to each instance. Imagine two bowls of soup, each with its own temperature and ingredients. Companion objects offer a different approach. They let you define properties that belong to the class itself, shared by all instances – like a universal recipe for the perfect bowl of soup!

Companion object properties might seem similar to static properties in other languages, but there are some key distinctions. You’ll delve into these differences in the next section.

Imagine you’re trying to set up a discount system. But you don’t want to lose all your profits and set the maximum available discount that any food can have.

  // companion object property
  companion object {
    val maxDiscount = 0.3
  }

You can use a companion object property to store the maximum discount value. Here, maxDiscount is a property on Food itself rather than the instances. That means you don’t access this property on an instance:

 cucumber = Food("Cucumber", "1.0", "US")

  // Error: Unresolved reference
  // Can't access members of the companion object on an instance
  println(cucumber.maxDiscount)

Instead, you access it on the class itself. Add this to your code:

  println(Food.maxDiscount) // 0.3

Using a companion object property means you can retrieve the same property value from anywhere in the code for your app or algorithm. The discount upper limit is accessible from any Food instance or any other place in the app, like the main menu.

When you are using Kotlin methods in Java code, you may want to access the companion object properties. To do that you should use the Companion subclass, and to make the code look nicer, you can use the @JvmStatic annotation, to force a property to be treated as static field of a class with static getters and setters.

Update Food to look like this alternative version:

  // companion object property
  companion object {

  @JvmStatic val maxDiscount = 0.3

  }

Now, from Java, you can access maxDiscount as follows:

Food.getMaxDiscount(); // Fine, thanks to @JvmStatic
Food.Companion.getMaxDiscount(); // Fine too, and necessary if @JvmStatic were not used

Delegated Properties

So far, property initialization has been pretty basic: setting a value directly, using defaults, or calculating it with custom accessors. But what if you need more power? Delegated properties come to the rescue! These handy tools, introduced with the by keyword, let you offload property initialization or behavior to another object. Please see the next section of the lesson to learn more about by keyword with a real-life example.

There are several reasons you might use delegated properties. Perhaps initialization is complex, and you want to delegate it to a specialist. Maybe the value won’t be known until later, so you want to delay initialization. Or you might want to be notified whenever a property changes. Delegated properties offer solutions for all these scenarios.

One common use case is lazy initialization. This is perfect for properties that are expensive to compute or whose value you won’t need right away. With lazy properties, the value is only calculated the first time it’s accessed.

Lateinit

Sometimes, a property might not have a value assigned immediately when a class instance is created. Maybe you’ll inject the value later, or it’s only needed under certain conditions. The lateinit keyword comes in handy here.

Using lateinit tells the compiler that the property is guaranteed to be initialized before it’s used. This avoids the need for null checks and keeps your code clean. But remember, with great power comes responsibility – you must ensure the property is initialized before accessing it; otherwise, you’ll get an exception!

Create this Shop class that has a Shelf property declared as a lateinit var:

class Shopkeeper

class Shop {
  lateinit var keeper: Shopkeeper
}

Remember, properties declared with lateinit can’t be val “read-only” because they don’t have a set value at initialization. The var keyword, “mutable” is required since the property’s value will be assigned later.

Since the lateinit property is initially unset, it’s crucial to initialize it before you use it. Failure to do so will result in an exception. The compiler won’t let you forget this vital step!

So, what if you acquired a shop and then discovered that the shop needed a shopkeeper?


val shop = Shop()
// ... shop has no shopkeeper, need to hire one!

println(shop.keeper)
// Error: kotlin.UninitializedPropertyAccessException:
// lateinit property keeper has not been initialized

// ... hired someone
shop.keeper = Shopkeeper()

If you try to access the lateinit keeper property before it’s been initialized, you’ll get an exception. Ouch!

Once you’ve assigned a value to keeper, you can finally, after a long month of making everything ready, open and close the shop and the shopkeeper will be there.

Extension Properties

Say you bought a shop, but the business isn’t doing well, and you decided to do a sale for all products. The discount is 30%, and you need to update the prices of all your food items. It would be nice not to do the calculation of a new price every time you see a food item.

Go back to your Food class:

class Food(val name: String, val price: Int)

But what if the food class is provided to you in a library, and you can’t modify its price? Kotlin uses extension properties to help you add such functionality without changing the class definition.

To add an extension property, create a new property with the property name appended to the class name, like so:

fun main() {
  val Food.newPrice: Double
   get() = 0.7 * price
}

You’ve created an extension property named newPrice on the Food class and provided a custom getter for newPrice. Extension properties don’t have backing fields, so you can only define them using custom accessors.

You can access the extension property like any other property defined within the class:

  val pear = Food("Pear", 10)
  println(pear.newPrice) // 7.0

Nice! You no longer have to calculate the discounted price, which would be price - price * 30%.

See forum comments
Download course materials from Github
Previous: Introduction Next: Demo