When the classes/objects with related functionalities belong together, they are like companions of each other. A companion means a partner or an associate, in this case.
Reasons for companionship
Cleaner top-level namespace
When some independent function is intended to be used with some specific class only, instead of defining it as a top-level function, we define it in that particular class. This prevents the pollution of top-level namespace and helps with more relevant auto-completion hints by IDE.
Packaging convenience
It's convenient to keep the classes/objects together when they are closely related to each other in terms of the functionality they offer to each other. We save the effort of keeping them in different files and tracking the association between them.
Code readability
Just by looking at the companionship, you get to know that this object
provides helper functionality to the outer class and may not be used in any other contexts. Because if it was to be used with other classes, it would be a separate top level class
or object
or function.
Primary purpose of companion object
Problem: companion class
Let's have a look at the kinds of problems the companion objects solve. We'll take a simple real world example. Say we have a class User
to represent a user in our app:
data class User(val id: String, val name: String)
And an interface
for the data access object UserDao
to add or remove the User
from the database:
interface UserDao {
fun add(user: User)
fun remove(id: String)
}
Now since the functionalities of the User
and implementation of the UserDao
are logically related to each other, we may decide to group them together:
data class User(val id: String, val name: String) {
class UserAccess : UserDao {
override fun add(user: User) { }
override fun remove(id: String) { }
}
}
Usage:
fun main() {
val john = User("34", "John")
val userAccess = User.UserAccess()
userAccess.add(john)
}
While this is a good setup, there are several problems in it:
- We have an extra step of creating the
UserAccess
object before we can add/remove a User
.
- Multiple instances of the
UserAccess
can be created which we don't want. We just want one data access object
(singleton) for User
in the entire application.
- There is a possibility of the
UserAccess
class to be used with or extended with other classes. So, it doesn't make our intent clear of exactly what we want to do.
- The naming
userAccess.add()
or userAccess.addUser()
doesn't seem very elegant. We would prefer something like User.add()
.
Solution: companion object
In the User
class, we just replace the two words class UserAccess
with the two other words companion object
and it's done! All the problems mentioned above have been solved suddenly:
data class User(val id: String, val name: String) {
companion object : UserDao {
override fun add(user: User) { }
override fun remove(id: String) { }
}
}
Usage:
fun main() {
val john = User("34", "John")
User.add(john)
}
The ability to extend interfaces and classes is one of the features that sets the companion objects apart from Java's static functionality. Also, companions are objects, we can pass them around to the functions and assign them to variables just like all the other objects in Kotlin. We can pass them to the functions that accept those interfaces and classes and take advantage of the polymorphism.
companion object
for compile-time const
When the compile-time constants are closely associated with the class, they can be defined inside the companion object
.
data class User(val id: String, val name: String) {
companion object {
const val DEFAULT_NAME = "Guest"
const val MIN_AGE = 16
}
}
This is the kind of grouping you have mentioned in the question. This way we prevent the top-level namespace from polluting with the unrelated constants.
companion object
with lazy { }
The lazy { }
construct is not necessary to get a singleton. A companion object
is by default a singleton, the object
is initialized only once and it is thread safe. It is initialized when its corresponding class is loaded. Use lazy { }
when you want to defer the initialization of the member of the companion object
or when you have multiple members that you want to be initialized only on their first use, one by one:
data class User(val id: Long, val name: String) {
companion object {
val list by lazy {
print("Fetching user list...")
listOf("John", "Jane")
}
val settings by lazy {
print("Fetching settings...")
mapOf("Dark Theme" to "On", "Auto Backup" to "On")
}
}
}
In this code, fetching the list
and settings
are costly operations. So, we use lazy { }
construct to initialize them only when they are actually required and first called, not all at once.
Usage:
fun main() {
println(User.list) // Fetching user list...[John, Jane]
println(User.list) // [John, Jane]
println(User.settings) // Fetching settings...{Dark Theme=On, Auto Backup=On}
println(User.settings) // {Dark Theme=On, Auto Backup=On}
}
The fetching statements will be executed only on the first use.
companion object
for factory functions
Companion objects are used for defining factory functions while keeping the constructor
private
. For example, the newInstance()
factory function in the following snippet creates a user by generating the id
automatically:
class User private constructor(val id: Long, val name: String) {
companion object {
private var currentId = 0L;
fun newInstance(name: String) = User(currentId++, name)
}
}
Usage:
val john = User.newInstance("John")
Notice how the constructor
is kept private
but the companion object
has access to the constructor
. This is useful when you want to provide multiple ways to create an object where the object construction process is complex.
In the code above, consistency of the next id
generation is guaranteed because a companion object
is a singleton, only one object will keep track of the id
, there won't be any duplicate id
s.
Also notice that companion objects can have properties (currentId
in this case) to represent state.
companion object
extension
Companion objects cannot be inherited but we can use extension functions to enhance their functionality:
fun User.Companion.isLoggedIn(id: String): Boolean { }
The default class name of the companion object
is Companion
, if you don't specify it.
Usage:
if (User.isLoggedIn("34")) { allowContent() }
This is useful for extending the functionality of the companion objects of third party library classes. Another advantage over Java's static
members.
When to avoid companion object
Somewhat related members
When the functions/properties are not closely related but only somewhat related to a class, it is recommended that you use top-level functions/properties instead of companion object
. And preferably define those functions before the class declaration in the same file as that of class:
fun getAllUsers() { }
fun getProfileFor(userId: String) { }
data class User(val id: String, val name: String)
Maintain single responsibility principle
When the functionality of the object
is complicated or when the classes are big, you may want to separate them into individual classes. For example, You may need a separate class to represent a User
and another class UserDao
for database operations. A separate UserCredentials
class for functions related to login. When you have a huge list of constants that are used in different places, you may want to group them in another separate class or file UserConstants
. A different class UserSettings
to represent settings. Yet another class UserFactory
to create different instances of the User
and so on.
That's it! Hope that helps make your code more idiomatic to Kotlin.