Companion Objects for handling Static and Factories in Kotlin

- Andrés Cruz

ES En español

Companion Objects for handling Static and Factories in Kotlin

Companion objects in Kotlin are, ultimately, the mechanism we have to create static properties or methods within classes. It's that simple.

It is important to understand that in Kotlin the reserved word static does not exist as in other languages. Therefore, we cannot directly create unique instances (singletons) using static. Instead, Kotlin offers us a much more flexible structure called a companion object, which is precisely what we are going to discuss in this article.

In the official Kotlin documentation there are many examples. If you search for “companion object Kotlin”, you will find the official reference with different use cases. Here, however, we are going to stick to the essentials.

Companion objects are the way in which Kotlin works with Java's static; another approach that you can use is the famous singleton type classes that are used a lot in applications, as is the case with Android, which is our special interest.

We assume that we already know how to create enumerated classes in Kotlin.

What is a Companion Object?

A companion object always lives inside a class.

This is important to remember.

Until now, when we wanted to use a class, we did the following:

  • We define the class.
  • We create an instance.
  • We access its methods or properties.

However, static methods or properties do not require instances. They are accessed directly from the class.

Since Kotlin does not have statics, this is handled by a companion object, which is the equivalent.

What do we use Companion Object for?

Mainly, I see two key uses:

  • Create static methods and properties
  • Use them as a Factory, that is, to create special constructors

Otherwise, the syntax may vary a bit depending on the case, but we are going to see several practical examples to make it clear.

When does it make sense to use it?

Static information

A good example is when you handle data that doesn't change:

  • Notification types (success, error, warning, etc.).
  • Global settings.
  • Unique data throughout the application.

For example, in an application you normally only have one authenticated user.

It makes no sense to be creating user instances all the time.

The logical thing is:

  • When starting the application, bring the user's information (for example, from the database).
  • Save it in a static structure.
  • Access it from anywhere in the app.

That's where a companion object fits perfectly.

In these cases, it makes no sense to be constantly creating instances. The ideal is to load the information only once and be able to access it from anywhere in the application.

Basic syntax

The syntax is quite simple:

  • The companion object goes inside the class.
  • The name is optional.
  • Within it you can define all the properties and methods that you want to be static.

We are going to see practical examples, but first I want to explain when it makes sense to use this.

Examples of the functioning of Companion Objects in Kotlin

Example with static data

In the example you see, we have information such as arrays, auxiliary functions and methods that do not change.

It makes no sense to create instances every time we want to access this data, because it is flat, static data.

Unlike the Person example, where it does make sense to create instances because the data varies (name, age, etc.).

To use Companion Objects in Kotlin, they must be declared within the instance of a class as follows:

class Persona {
   companion object Raza {
       // clasificacion persona
       val edades: IntArray = intArrayOf(0, 2, 30, 60)
       var razas =
               arrayOf("Europeo", "Asiatico", "Latino", "Arabe", "Africano", "Americano", "Canadiense")
       var clasificacion = "Clasificación personas"
       init {
           println("Inicializando el companion")
       }
       fun enAmerica(): Array<String> {
           return arrayOf(razas[2], razas[5], razas[6])
       }
       fun adultoMayor(): Int {
           return edades[3]
       }
       fun printClasificacion() {
           println(clasificacion)
       }
   }
}

In the first instance, it would seem that using a more complex structure, since we are nesting elements, in this case our class within a Companion Object, and it is true, but it gains in readability, since it is very common that we want to define an important block as static, and we do not have to be placing the word static every so often to define multiple functions, or parameters as we did in Java.

With the grouping that we do with the Companion Object, it is more than enough to group all our static methods and properties at once; in Java we would have to do the following.

public static final String KEY = "key";

Now, if we have several structures and variables that we want to be static, then everything gets quite complicated, because we have to be placing static every so often, and if we also have methods and attributes that are not static, in the end what we have is a good mix of several things; something that does not happen with Kotlin when grouping in a block of code the properties and functions that we want to be static through the Companion Objects.

In the previous example, we can see that we declare our Companion Object within the Person class, as a fundamental principle, to create and subsequently use a Companion Object, it must be declared within a class as we did previously, that we declare the Companion Object called Raza within the mother class Person.

Accessing the methods of the Companion Object

With the Companion Object already declared, we create some properties such as ages and races that are Arrays which we create personalized methods for each scope in this example, that is, we create as an example a method inAmerica() that returns an Array with the native races of the American continent, which would be those of the Latinos, the Americans (North America) and the Canadians.

We do something similar with the ages, for which we create a method called olderAdult() that returns the age from which it is considered that the person is an older adult.

We also create a text using a variable called classification that returns its value through the printClassification method.

Finally, we can use a constructor defined as init if necessary.

Variations in the structure of the Companion Object to handle statics

We can omit the name of the Companion Object from the declaration, leaving it as follows:

class Persona {
   companion object {
       // clasificacion persona
       val edades: IntArray = intArrayOf(0, 2, 30, 60)
       var razas =
               arrayOf("Europeo", "Asiatico", "Latino", "Arabe", "Africano", "Americano", "Canadiense")
       var clasificacion = "Clasificación personas"
/*Demas metodos*/
}

To be able to use methods or properties of the Companion Object we use the following scheme; taking the previous code as an example, we have that:

println(Persona.enAmerica()[0]) println(Persona.printClasificacion()) println(Persona.adultoMayor()) }

Accessing the properties of the Companion Object

To reference the methods we saw earlier; of course, we can directly reference the properties as we have done in previous entries, and this is excellent, since we do not have to create the famous get and set methods for each property of our object; declaring the property and its type is a necessary and sufficient condition:

Persona.edades
Persona.razas
Persona.clasificacion

As we can see, we must use the name of the mother class followed by the method or property defined within the Companion Object, it is the same approach as that used in Java when referencing a method or property of a static class in Java; that is, we first reference the name of the class to then reference the method or property of the Companion Object without the need to create an object of the class.

Methods of the mother class of the Companion Object

We can define the structure of our class as we want, taking as a reference the structure we saw in one about the use of classes.

We have that our code would look like this:

class Persona {
   var nombre: String = ""
   var apellido: String = ""
   var edad: Int = 0
   init {
       this.nombre = nombre
       this.apellido = apellido
       this.edad = edad
   }
   companion object Raza {
       // clasificacion persona
       val edades: IntArray = intArrayOf(0, 2, 30, 60)
       var razas =
               arrayOf("Europeo", "Asiatico", "Latino", "Arabe", "Africano", "Americano", "Canadiense")
       var clasificacion = "Clasificación personas"
       init {
           println("Inicializando el companion")
       }
       fun enAmerica(): Array<String> {
           return arrayOf(razas[2], razas[5], razas[6])
       }
       fun adultoMayor(): Int {
           return edades[3]
       }
       fun printClasificacion() {
           println(clasificacion)
       }
   }
}

Example: Database

A very classic example is a database.

The connection parameters:

  • Version
  • Name
  • Configuration

Here we have a simple example of a class with a companion object:

 
class BaseDeDatos {
    companion object {
        const val VERSION = 1
        
        fun obtenerNombre(): String {
            return "MiApp_DB"
        }
    }
}

To use it, we do not create an instance: 

println(BaseDeDatos.VERSION)
println(BaseDeDatos.obtenerNombre())

This makes perfect sense, since this information will not vary during the execution of the program.
Unlike, for example, user data, which can change (age, name, etc.).

They should not change during the execution of the program.

That's why:

  • We create the class.
  • We define a companion object without a name (because we don't need it).
  • We place the static information there.

To access it, we simply do:

  • Database.version
  • Database.getName()

Without creating instances.

It makes perfect sense that these properties are constant, since they do not vary during execution.

Factory: Special Constructors

Now let's go with another very important use: the factory.

What is a Factory?

A factory is a structure that allows us to create objects in a controlled way.

This is closely linked to object-oriented programming, where we use classes to represent real-world objects:
a user, a car, a house, a pizza.

Another extremely common use of the companion object is as a Factory, that is, as an object factory.

Pizza example

Imagine a pizzeria:

  • There is a menu.
  • You can order pepperoni, cheese or margherita pizza.
  • You can't ask for things that don't exist.

When you order a pizza:

  • The process is already optimized.
  • The ingredients are already prepared.
  • You don't start from scratch every time.

That is exactly what a factory does.

Applied to programming

Suppose you have users:

  • Normal user.
  • Administrator user.
  • Guest user.

It makes no sense to repeat the same validation logic in all application modules:

  • Dashboard
  • Registry
  • API
  • Mobile app

A factory centralizes all that logic in one place.

class User private constructor(
    val name: String,
    val role: String,
    val permissions: List<String>
) {
    companion object {
        fun createGuest(name: String): User {
            return User(name, "Guest", listOf("Read"))
        }
        fun createAdmin(name: String): User {
            return User(name, "Admin", listOf("Read", "Write", "Delete"))
        }
    }
    fun showInfo() {
        println("Usuario: $name | Rol: $role | Permisos: $permissions")
    }
}

Here the constructor is private, which prevents invalid users from being created.

All the logic is centralized in one place.

val guest = User.createGuest("Carlos")
val admin = User.createAdmin("Ana")
guest.showInfo()
admin.showInfo()

This avoids having repeated validations in different modules of the application.

Factory with Companion Object

In the example:

  • The constructor is private.
  • No one can create users directly.
  • They can only be created using the methods of the companion object.

This avoids errors such as:

  • Invalid roles.
  • Incorrect permissions.
  • Misconfigured users.

Each factory method creates a user with a valid and well-defined structure.

Advantages of the approach

  • All the logic is in one place.
  • You avoid duplicate validations.
  • The code is cleaner and more maintainable.
  • It is impossible to create invalid objects by accident.

Exactly the same as in the pizzeria:

  • You don't go to the supermarket every time someone orders a pizza.

Singleton + Factory

In this case:

  • The companion object acts as a factory.
  • The class can have multiple instances.
  • But the creation logic is unique.

Then, once the instance is created, you can access the normal methods of the class.

Factory with Singleton: Notifications

Finally, Kotlin also offers the keyword object, which creates a real singleton.

A difference from the companion object:

  • It is not inside a class.
  • There is only one instance in the entire application.

This is very useful when you want to create, for example:

  • A NotificationFactory.
  • A global service manager.

Here we combine Factory + Singleton using object:

 
interface Notificacion {
    fun enviar(mensaje: String)
}
class NotificacionEmail : Notificacion {
    override fun enviar(mensaje: String) =
        println("Enviando Email: $mensaje")
}
class NotificacionSMS : Notificacion {
    override fun enviar(mensaje: String) =
        println("Enviando SMS: $mensaje")
}
object NotificacionFactory {
    fun crearNotificacion(tipo: String): Notificacion {
        return when (tipo) {
            "EMAIL" -> NotificacionEmail()
            "SMS" -> NotificacionSMS()
            else -> throw IllegalArgumentException("Tipo desconocido")
        }
    }
}

Use:

val notif = NotificacionFactory.crearNotificacion("EMAIL")
notif.enviar("¡Hola desde la Factory!")

Here we have:

  • A single instance (object)
  • Ability to create multiple types of objects
  • A common interface (Notificacion) that guarantees a shared method

Summary

Here we see a more advanced case:

  • A Notification interface.
  • Different implementations: email, SMS, push.
  • A singleton that decides what type of notification to create.

From the outside, you just call:

  • createNotification(type)
  • send(message)

You don't care how each type of notification works internally.

Main advantage

The great advantage of this whole approach is that:

  • You centralize the logic.
  • You work with clear structures.
  • You avoid errors.
  • The code is easy to read and maintain.

Quick summary

  • Kotlin does not have static
  • companion object is its equivalent
  • It serves to:
    • Static methods and properties
    • Singletons
    • Factory Methods
  • Centralizes logic
  • Avoids errors and duplication
  • Makes the code cleaner and more maintainable

Conclusion

  • companion object: statics within a class.
  • object: real singleton.
  • Factory: controlled creation of objects.
  • Everything can be combined depending on the case.

I hope it is now much clearer how each one works and when to use each pattern.

All the examples we saw previously are Singleton classes, which by definition are classes with a single instance in the entire scope of the application; they are widely used to have a single connection with the database and avoid saturation or the famous race conditions (as could happen to us in Android with the SQLite database), user data and a long etc.

As you can see, Companion Objects are very useful when we work with structures that must have a single instance, such as user data, or database connection data in Android.

You can consult the official documentation at the following link: Companion Objects

The next step, Arrays and lists in Kotlin: First steps these mutable and immutable structures.

In this post we will talk about the companion objects that have been the way in which Kotlin works with the static ones of Java.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español