10

Computed properties with property getters | OkKotlin

 3 years ago
source link: https://okkotlin.com/computed-properties/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Computed properties with property getters

December 15, 2019

/

7 min read

/

#clean

#transformations

Custom getters and setters for fields in a class is not a new idea. We have been using them since our early days in programming.

Kotlin, however, takes that concept to a more elegant level by allowing us to define getters and setters at the declaration site of our properties.

The primary advantage of this feature is the immediate connection we can make between a declared property and the logic that transforms the property.

This week, we will be discussing how we can make our code more meaningful and easy to decipher using property getters and setters, a.k.a. computed properties.

Computed properties make beautiful code

Like type aliases, the best value in using computed properties is to have a slick and readable codebase.

There's nothing extra that we can do with a computed property that we cannot do with a plain old method. We can define our own getter and setter methods to make fields return a computed value.

The problem with having logic inside our getter methods is that if we mistakenly access the field value instead of its getter, we are skipping our business logic. This approach will result in miscalculations or a lousy output.

Having logic inside Kotlin's property getters ensure that there is only one way to access a property, and it will return the correct value every time.

Here's an example:

var formattedSpeed: String
    get(): { return "$field km/h" }

Now, every time we access formattedSpeed, we will get a formatted speed value indicating the unit. We don't have to do a string interpolation ourself every time we set a speed value.

formattedSpeed = "55"
println("Speed is $formattedSpeed")
    
// Output: Speed is 55 km/h

This example above might seem like a lame one, but it's just a starting point to get creative.

We can also create virtual properties

Virtual properties aren't actual fields that you have in a class but are merely wrappers on top of some other property.

The following example will clear things up:

Suppose we have to build a content management system which lets our users manage multiple publications from a single dashboard. If we want to have a section where we show up all posts across publications, we need to run a loop like the following one everywhere we want the posts to show up:

val allPosts = mutableListOf<Post>()
    
publications.forEach {
    allPosts.addAll(it.posts)
}

To make this logic reusable, we can wrap it inside a function:

fun getAllPosts(): List<Post> {
    val allPosts = mutableListOf<Post>()
    
    publications.forEach {
        allPosts.addAll(it.posts)
    }
    
    return allPosts
}

Now, here's a trick. For simple cases like the aggregation shown above, it's way better to declare a computed property like this:

val allPosts: List<Post>
    get() {
        val aggregate = mutableListOf<Post>()
    
        publications.forEach {
            aggregate.addAll(it.posts)
        }
    
        return aggregate
    }

This change makes accessing all posts from across publication a simple reference to our new virtual property called allPosts.

allPosts.forEach {
    println(it.title)
}
    
// Output: Prints out all blog posts titles

The reason I'm calling this a virtual property is because this property is not an actual field defined in the class. It's just a wrapper which returns a value calculated from other properties in the class.

Also, if we look closely, there's nothing new that we did with a computed property that we couldn't have done with the function getAllPosts(). Defining the property just made our code much more elegant and predictable.

Handle computed properties with care

The allPosts computed property we just created, can be a hidden mess, if not treated carefully. From a 35,000 ft view all seems reasonable, but if we look closely, each property access is of the time complexity O(N2).

A little knowledge of algorithms is enough to tell that this is bad. Sure, computing has evolved, and it's getting faster than ever. However, in some instances, this code can lag our super fast smartphone to an extent where it's noticeable.

Take Android's RecyclerView as an example. When a RecyclerView populates a list of data on the screen, it repeatedly calls the bindViewHolder() method as the user scrolls through the list.

List of items

A heavy operation such as accessing a computed property which takes O(N2) time inside bindViewHolder() can easily cause visible jank while scrolling through a list.

This broken experience can go unnoticed when our data set is small, let's say our list contains only ten items. But, let's face it; real-world apps deal with way more data than that. One hidden bug can lead to painful debugging time.

If we want or need to use a computed property in these cases, we can either:

  • Make sure the computation logic is fast, or
  • Compute once and pass the result during our class's initialisation

In the previous case, instead of accessing the computed property on every bindViewHolder() call, we can pass a previously computed value to the adapter's constructor, like this:

class PostsListAdapter(private val allPosts: List<Post>)

The computation logic inside the allPosts is run only once when we initialise our PostsListAdapter, no matter how many times the bindViewHolder() method is called.

Making them reactive

Using computed properties in Vue.js, I was pretty accustomed to the fact that computed properties were de-facto reactive.

Reactive means that if a computed property depends on some other property, when that property changes, the computed property gets recalculated. This behaviour usually triggers a view change with the updated result.

In Kotlin, sadly, that's not the case.

Recalculation will only be done once the property value is set or accessed depending on where we have written our calculation logic.

We can achieve a reactive behaviour by using Kotlin's property observer delegate, though. It's a patch but let's see how it can be done.

We need a backing property which we will keep updating and route the output through our computed property.

Let's say we need to display a formatted speed value to the user whenever the speed gets updated. Here's how we can sprinkle in some reactivity:

private var _formattedSpeed: String by Delegates.observable("0") { _, _, _ ->    
    displaySpeed(formattedSpeed)
}
    
val formattedSpeed: String    
    get() = "$_formattedSpeed km/h"

Here, we are observing a private counterpart of our computed property, which we call the "backing property". Whenever our backing property changes, we display the change to the user.

A little detail here is, on every change of the backing property value, we are querying the value of the computed property and passing it down to our displaySpeed() method.

This approach ensures that the updated value goes through the computed property, and necessary transformations are applied before it reaches the user.

You'll rarely need to rely on this technique. However, it could be handy to be aware of this possibility.

Uh-oh! The stack is full

Stacks are nice, but no one wants to overflow their stacks.

Similar to recursion, it's pretty easy to get hit on the face with a StackOverflowException if we don't write our computation logic well.

In Kotlin, any property we define is not a direct reference to a memory address. This means that every time we access value from a property, it comes through the get() method.

A code like this is a sure-shot way to get a StackOverflowException:

val formattedSpeed: String    
    get() = "$formattedSpeed km/h"

Every time we access formattedSpeed, we call it's get() method. Also, in the get() method we are trying to access formattedSpeed, which in turn calls the get() method again. This process continues until the device's stack can't hold any more method calls.

Thus, the dreaded StackOverflowException.

If we need to access the computed property's actual value, we need to access its backing field instead of the property.

The above code becomes:

var formattedSpeed: String = ""
    get() = "$field km/h"

Here, field is the keyword used to access the actual value of a property. Think of it as a direct reference to the memory.

It's essential to be aware of this common mistake to avoid losing time in debugging.

Making the most out of it

Just because computed properties exist, doesn't mean we should never write a simple property again. Language features such as this are most useful when there's a particular use case for them. Like the ones, we discussed above.

Look at your code, be creative and think if using computed properties somewhere makes your code better. If not, well, stick to the plain old val goodByeMessage = "Sayonara!" .

Here's a sketch note on the topic

Computed properties sketch note

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK