Instruction
Throughout this lesson, you’ll learn:
- About companion objects and their role in organizing class-related data and functions.
- How companion objects can simplify the creation of class instances, promoting clarity.
- How to incorporate companion objects into code.
How Do You Declare a Companion Object?
Declaring and using a companion object is remarkably simple. You only need a companion object block of code for your class. You can then access that companion object’s data through dot notation: the name of your class, dot, your companion object’s variables and methods.
Here’s a simple example of declaring a companion object within a Circle class:
class Circle(val radius: Double) {
companion object {
const val PI = 3.14159
fun calculateArea(radius: Double): Double {
return PI * radius * radius
}
}
}
In this example, you can declare a companion object to the class Circle. This companion object stores some fundamental data i.e., the value of pi and a method called calculateArea to calculate the area of a circle.
To call your companion object, all you need is:
val area = Circle.calculateArea(5.0)
println("The area of the circle is ${area}")
println("PI from companion object: ${Circle.PI}")
If you look at the area value, you’ll see it’s correct, given the radius you supplied.
Companion Object Use Cases
Companion objects are that easy to use. However, don’t let their simplicity deceive you. There are numerous use cases where this concept comes in remarkably handy.
Factory Methods
Developers frequently leverage Kotlin companion objects for factory methods. Consider the following scenario. Suppose you’re writing a blogging app with three different types of users:
- Administrators can do anything with the app. They can delete posts, upload media, review comments, and more.
- Editors can review and modify posts but can’t do anything else.
- Contributors can create new posts and upload media, but they can’t publish anything without an editor or an administrator signing off on it.
In your app, you want to represent these users with different classes. They all inherit from the same base class, User, but each user class will have its unique methods and properties.
Suppose you code your Users like this:
sealed class User(val username: String)
class AdminUser(username: String) : User(username)
class EditorUser(username: String) : User(username)
class ContributorUser(username: String) : User(username)
As you can see, you have three classes, each inheriting from the base User class, representing your app’s three types of users.
The problem is you need a clean and easy way to create a new user for each user type. You don’t want to keep writing a bunch of when or if statements throughout your app to execute conditional logic based on the user type. Instead, consolidating that logic into one factory method makes the most sense.
You could create a class called UserManager with a companion object to create the user appropriately. Here’s an example of how you could do that:
class UserManager {
companion object {
fun createUser(username: String, userType: String): User {
return when (userType.toLowerCase()) {
"admin" -> AdminUser(username)
"editor" -> EditorUser(username)
else -> ContributorUser(username)
}
}
}
}
In the example above, you leverage Kotlin companion objects to create a new user easily. Anywhere you need to create a new user, you can call UserManager.create with the appropriate parameters.
Data Validation
Data validation is another significant use case for Kotlin companion objects.
Many apps require the user to enter their address. Maybe the app you’re building may have a shipping component, and you need the user to enter their shipping address. Or, say you’re writing a banking app, and the address is part of the profile. Either way, suppose you’re working on a Kotlin app requiring users to enter their addresses.
Typically, you’d want to validate the address before saving it in your app or a database. It’s easy for users to enter the wrong address, like an incorrect postal code or street name. Suppose you have an API that you can call to validate the user’s address.
There are a couple of ways you could implement this. First, you could have a public constructor and some form of a method called validate. The validate method would take in the address object you created and return true or false depending on whether the address is correct. The problem with this approach is that it could be cleaner.
Instead, consider leveraging Kotlin companion objects to validate for you and privately construct the class. That way, the only way for a developer to create the class would be to pass it to a valid address. This paradigm is powerful because it means developers know an Address object contains a valid address.
Here’s how you would implement something like this in code:
class Address private constructor(
private val street: String,
private val city: String,
private val zipCode: String
) {
companion object {
fun createAddress(street: String, city: String, zipCode: String): Address? {
if (!AddressValidationAPI.isValidAddress(street, city, zipCode)) {
return null // Address failed validation
}
return Address(street, city, zipCode)
}
}
}
It’s straightforward to see how this logic works. The companion object can access the private constructor because it’s an entire object and not just a static method. It can instantiate the Address class if the address validation succeeds.
Helper Methods
Another everyday use case for companion objects involves creating utility or helper functions. Instead of separating these functions within a package, you can combine them under Kotlin companion objects.
Suppose you need a method called normalizeSpaces that takes in text and removes any extra spaces between words. Such a method may be helpful in a writing or blogging app. Maybe you want to ensure that posts written to your site don’t have any extra spaces to ensure your site has a clean look.
In Kotlin, you could create a static class containing these utility functions. However, you could also make a companion object. The companion object would have a clean implementation, better align with Kotlin’s ideology, and allow for inheritance. You may want to put your class in a package and extend it with other functions only applicable to a new project. Using a companion object lets you do that.
You could write the code to look like this:
class StringUtils {
companion object {
fun normalizeSpaces(text: String): String {
return text.trim().replace(Regex("\\s+"), " ")
}
}
}
You can use a companion object in this code block to implement the space-checking capability. Then, you can call that code like you would a static function throughout the app. It’s clean and has very straightforward maintenance.