Knowing this Kotlin pitfall can save you from bugs
Kotlin is an awesome language, but its “syntax sugar” can sometimes be confusing. Although this particular thing is mentioned in the documentation, it’s easy to miss. Let’s start with an example:
We have a class AspectRatioScreen
with one property, which defaults to old good 4:3
. Then we have ResolutionScreen
with width
and height
properties. Their setters automatically update the aspect ratio.
In main we create a ResolutionScreen
object and print its parameters. By default, the resolution is set to 1920x1080
which corresponds to 16:9
aspect ratio. But the program output is:
Screen resolution: 1920x1080, ratio: 4/3
The aspect ratio doesn’t match the resolution. Why? The Kotlin documentation says:
If you define a custom setter, it will be called every time you assign a value to the property, except its initialization.
This means that setting width
’s and height
`s initial dimensions doesn’t call their setters. One way to fix it would be to add a init
block inside the ResolutionScreen
class:
class ResolutionScreen : AspectRatioScreen() {
var width = 1920
// ...
var height = 1080
// ...
init {
width = 1920
height = 1080
}
}
This will re-assign the default values and call setters. The displayed result is now correct:
Set width to 1920
Set height to 1080
Screen resolution: 1920x1080, ratio: 16/9
This solution has one drawback: You have to declare the default value twice because properties cannot be left without an initializer. A workaround would be to define constants: private const val DEFAULT_WIDTH = 1920
and using them in both places.
The simplest solution is to call reacalculateAspectRatio()
directly inside the init
block:
class ResolutionScreen : AspectRatioScreen() {
var width = 1920
// ...
var height = 1080
// ...
init {
recalculateAspectRatio()
}
}
This is the clearest solution for this particular situation. There’s another way of doing this, which looks better when there is a single property, for which we want to initialize its backing property:
class Duration {
var durationInSeconds: Int = 1
}class EnhancedDuration : Duration() {
var durationInHours = 1.also { durationInSeconds = 3600 * it }
// ...
}
This syntax has one advantage — we initialize the backing property (durationInSeconds
in this case) in the same place we initialize the original property.
In the case of our example it would look like this:
class ResolutionScreen : AspectRatioScreen() {
var width = 1920
set(value) {
field = value
recalculateAspectRatio()
}
var height = 1080.also { aspectRatio = width divBy it }
set(value) {
field = value
recalculateAspectRatio()
}
// ...
}
The also
block is only present in the height
initializer, because the initialization goes from top to bottom — we cannot use width
nor height
before they’re initialized. Even inside the also
block, height
cannot be used, because it’s yet to be initialized. That’s why in cases where two or more properties are somehow related, it’s better to use init
blocks.