Chapters

Hide chapters

Kotlin Apprentice

Second Edition · Android 10 · Kotlin 1.3 · IDEA

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

13. Properties
Written by Ben Morrow & Joe Howard

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In Chapter 11, you were introduced to properties as the data members of Kotlin classes and objects.

In the example below, the Car class has two properties, both constants that store String values:

class Car(val make: String, val color: String)

The two properties of Car are supplied in the primary constructor, and they store different string values for each instance of Car.

Properties can also be set up with custom accessors, also known as getters and setters. The properties supplied in the class primary constructor use default implementations of the accessors, storing the data in a backing field.

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

Constructor properties

As you may have guessed from the example in the introduction, you’re already familiar with many of the features of properties. To review, imagine you’re building an address book. The common unit you’ll need is a Contact.

class Contact(var fullName: String, var emailAddress: String)

You can use this class over and over again to build an array of contacts, each with a different value. The properties you want to store are an individual’s full name and email address.

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

You create an object by passing values as arguments into the class primary constructor:

val contact = Contact(fullName = "Grace Murray", emailAddress = "grace@navy.mil")

As with any function call not using default values, using named arguments in the primary constructor is optional. You can access the individual properties using dot notation:

val name = contact.fullName // Grace Murray
val email = contact.emailAddress // grace@navy.mil

You can assign values to properties as long as they’re defined as variables. When Grace married, she changed her last name:

contact.fullName = "Grace Hopper"
val grace = contact.fullName // Grace Hopper

If you’d like to prevent a value from changing, you can define a property as a constant instead using val.

class Contact2(var fullName: String, val emailAddress: String)

var contact2 = Contact2(fullName = "Grace Murray", emailAddress = "grace@navy.mil")

// Error: Val cannot be reassigned
contact2.emailAddress = "grace@gmail.com"

Once you’ve initialized an instance of the Contact2 class, you can’t change emailAddress.

Default values

If you can make a reasonable assumption about what the value of a property should be when the type is initialized, you can give that property a default value.

class Contact3(
  var fullName: String,
  val emailAddress: String,
  var type: String = "Friend")
var contact3 = Contact3(fullName = "Grace Murray", emailAddress = "grace@navy.mil")

Property initializers

Properties can also be initialized outside of the primary constructor, using literals and values passed into the primary constructor, using a property initializer.

class Person(val firstName: String, val lastName: String) {
  val fullName = "$firstName $lastName"
}
val person = Person("Grace", "Hopper")
person.fullName // Grace Hopper
class Address {
  var address1: String
  var address2: String? = null
  var city = ""
  var state: String

  init {
    address1 = ""
    state = ""
  }
}
val address = Address()

Custom accessors

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

Custom getter

The measurement for a TV is the perfect use case for a custom accessor. The industry definition of the screen size of a TV isn’t the screen’s height or width, but its diagonal measurement:

class TV(var height: Double, var width: Double) {
  // 1
  val diagonal: Int
    get() {
      // 2
      val result = Math.sqrt(height * height + width * width)
      // 3
      return result.roundToInt()
    }
}

val tv = TV(height = 53.93, width = 95.87)
val size = tv.diagonal // 110
tv.width = tv.height
val diagonal = tv.diagonal // 76

Mini-exercise

Do you have a television or a computer monitor? Measure the height and width, plug it into a TV object, and see if the diagonal measurement matches what you think it is.

Custom setter

The property you wrote in the previous section is a 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. As the property has no place to store a value, the setter usually sets one or more related other properties indirectly:

// 1
var diagonal: Int
  // 2
  get() {
    val result = Math.sqrt(height * height + width * width)
    return result.roundToInt()
  }
  set(value) {
    // 3
    val ratioWidth = 16.0
    val ratioHeight = 9.0
    // 4
    val ratioDiagonal = Math.sqrt(ratioWidth * ratioWidth + ratioHeight * ratioHeight)
    height = value.toDouble() * ratioHeight / ratioDiagonal
    width = height * ratioWidth / ratioHeight
  }
tv.diagonal = 70
println(tv.height) // 34.32...
println(tv.width)  // 61.01...

Companion object properties

In the previous section, you learned how to associate properties with instances of a particular class. The properties on your instance of TV are separate from the properties on my instance of TV.

class Level(val id: Int, var boss: String, var unlocked: Boolean) {
  companion object {
    var highestLevel = 1
  }
}

val level1 = Level(id = 1, boss = "Chameleon", unlocked = true)
val level2 = Level(id = 2, boss = "Squid", unlocked = false)
val level3 = Level(id = 3, boss = "Chupacabra", unlocked = false)
val level4 = Level(id = 4, boss = "Yeti", unlocked = false)
// Error: Unresolved reference
// Can't access members of the companion object on an instance
val highestLevel = level3.highestLevel
val highestLevel = Level.highestLevel // 1
class Level(val id: Int, var boss: String, var unlocked: Boolean) {
  companion object {
    @JvmStatic var highestLevel = 1
  }
}
Level.getHighestLevel() // Fine, thanks to @JvmStatic
Level.Companion.getHighestLevel() // Fine too, and necessary if @JvmStatic were not used

Delegated properties

Most of the property initialization you’ve seen so far has been straightforward. You provide an initializer for a property, for example, as a literal value or default value, or you use custom accessors to compute the value.

Observable properties

For your Level implementation, it would be useful to automatically set the highestLevel when the player unlocks a new one. For that, you’ll need a way to listen to property changes. Thankfully, you can use a delegated property observable to provide a callback for when the property changes.

class DelegatedLevel(val id: Int, var boss: String) {
  companion object {
    var highestLevel = 1
  }
  var unlocked: Boolean by Delegates.observable(false) {
    _, old, new ->
    if (new && id > highestLevel) {
      highestLevel = id
    }
    println("$old -> $new")
  }
}
val delegatedlevel1 = DelegatedLevel(id = 1, boss = "Chameleon")
val delegatedlevel2 = DelegatedLevel(id = 2, boss = "Squid")

println(DelegatedLevel.highestLevel) // 1

delegatedlevel2.unlocked = true

println(DelegatedLevel.highestLevel) // 2

Limiting a variable

You can also use delegated property observers to limit the value of a variable. Say you had a light bulb that could only support a maximum current flowing through its filament:

class LightBulb {
  companion object {
    const val maxCurrent = 40
  }
  var current by Delegates.vetoable(0) {
    _, _, new ->
    if (new > maxCurrent) {
      println("Current too high, falling back to previous setting.")
      false
    } else {
      true
    }
  }
}
val light = LightBulb()
light.current = 50
var current = light.current // 0
light.current = 40
current = light.current // 40

Lazy properties

If you have a property that might take some time to calculate, you don’t want to slow things down until you actually need the property. Say hello to lazy properties.

class Circle(var radius: Double = 0.0) {
  val pi: Double by lazy {
    ((4.0 * Math.atan(1.0 / 5.0)) - Math.atan(1.0 / 239.0)) * 4.0
  }
  val circumference: Double
    get() = pi * radius * 2
}

val circle = Circle(5.0) // got a circle, pi has not been run
val circumference = circle.circumference // 31.42
// also, pi now has a value

Mini-exercises

Of course, you should absolutely trust the value of pi from the standard library. It’s a constant in the standard library, and you can access it as kotlin.math.PI. Given the Circle example above:

lateinit

If you just want to denote that a property will not have a value when the class instance is created, then you can use the lateinit keyword.

class Lamp {
  lateinit var bulb: LightBulb
}
val lamp = Lamp()
// ... lamp has no lightbulb, need to buy some!

println(lamp.bulb)
// Error: kotlin.UninitializedPropertyAccessException:
// lateinit property bulb has not been initialized

// ... bought some new ones
lamp.bulb = LightBulb()

Extension properties

A circle has a radius, diameter and circumference that are all related to one another. But the Circle class above only includes the radius and circumference. It would be nice if the circle could tell you its diameter too, without you having to perform the calculation every time.

val Circle.diameter: Double
  get() = 2.0 * radius
val unitCircle = Circle(1.0)
println(unitCircle.diamater) // > 2.0

Challenges

Challenge 1

Rewrite the IceCream class below to use default values and lazy initialization:

class IceCream {
  val name: String
  val ingredients: ArrayList<String>
}

Challenge 2

At the beginning of the chapter, you saw a Car class. Dive into the inner workings of the car and rewrite the FuelTank class below with delegated property observer functionality:

class FuelTank {
  var level = 0.0 // decimal percentage between 0 and 1
}

Key points

Where to go from here?

You saw the basics of properties while learning about classes, and now you’ve seen the more advanced features they have to offer. You’ve already learned a bit about methods in the previous chapters and will learn even more about them in the next one!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now