Índice de contenido
- ¿Qué es un Companion Object?
- ¿Para qué usamos Companion Object?
- ¿Cuándo tiene sentido usarlo?
- Sintaxis básica
- Ejemplos del funcionamiento del Companion Objects en Kotlin
- Ejemplo con datos estáticos
- Accediendo a los métodos del Companion Object
- Variaciones en la estructura del Companion Object para manejar los static
- Accediendo a las propiedades del Companion Object
- Métodos de la clase madre del Companion Object
- Ejemplo: Base de datos
- Factory: Constructores especiales
- ¿Qué es un Factory?
- Ejemplo de la pizza
- Aplicado a programación
- Factory con Companion Object
- Ventajas del enfoque
- Singleton + Factory
- Factory con Singleton: Notificaciones
- Resumen
- Ventaja principal
- Resumen rápido
- Conclusión
Los companion object en Kotlin vienen siendo, en definitiva, el mecanismo que tenemos para crear propiedades o métodos estáticos dentro de las clases. Así de simple.
Es importante entender que en Kotlin no existe la palabra reservada static como en otros lenguajes. Por lo tanto, no podemos crear directamente instancias únicas (singletons) usando static. En su lugar, Kotlin nos ofrece una estructura mucho más flexible llamada companion object, que es justamente lo que vamos a tratar en este artículo.
Los companion objects que viene siendo la forma en la que Kotlin trabaja con los static de Java; otro enfoque que le puedes colocar, son las famosas clases tipo singleton que son empleadas mucho pero mucho en las aplicaciones como lo es el caso de Android que es nuestro especial interés.
Partimos que ya sabemos como crear las clases enumeradas en Kotlin.
¿Qué es un Companion Object?
Un companion object siempre vive dentro de una clase.
Esto es importante recordarlo.
Hasta ahora, cuando queríamos usar una clase, hacíamos lo siguiente:
- Definimos la clase.
- Creamos una instancia.
- Accedemos a sus métodos o propiedades.
Sin embargo, los métodos o propiedades estáticas no requieren instancias. Se acceden directamente desde la clase.
Como Kotlin no tiene estáticos, esto se maneja mediante companion object, que viene siendo el equivalente.
¿Para qué usamos Companion Object?
Principalmente, yo veo dos usos clave:
- Crear métodos y propiedades estáticas
- Usarlos como Factory, es decir, para crear constructores especiales
Por lo demás, la sintaxis puede variar un poco dependiendo del caso, pero vamos a ver varios ejemplos prácticos para que quede claro.
¿Cuándo tiene sentido usarlo?
Información estática
Un buen ejemplo es cuando manejas datos que no cambian:
- Tipos de notificaciones (success, error, warning, etc.).
- Configuraciones globales.
- Datos únicos en toda la aplicación.
Por ejemplo, en una aplicación normalmente solo tienes un usuario autenticado.
No tiene sentido estar creando instancias del usuario todo el tiempo.
Lo lógico es:
- Al iniciar la aplicación, traer la información del usuario (por ejemplo, desde la base de datos).
- Guardarla en una estructura estática.
- Acceder a ella desde cualquier parte de la app.
Ahí es donde un companion object encaja perfectamente.
En estos casos, no tiene sentido estar creando instancias constantemente. Lo ideal es cargar la información una sola vez y poder acceder a ella desde cualquier parte de la aplicación.
Sintaxis básica
La sintaxis es bastante simple:
- El companion object va dentro de la clase.
- El nombre es opcional.
- Dentro de él puedes definir todas las propiedades y métodos que quieras que sean estáticos.
Vamos a ver ejemplos prácticos, pero antes quiero explicarte cuándo tiene sentido usar esto.
Ejemplos del funcionamiento del Companion Objects en Kotlin
Ejemplo con datos estáticos
En el ejemplo que ves, tenemos información como arrays, funciones auxiliares y métodos que no cambian.
No tiene sentido crear instancias cada vez que queremos acceder a estos datos, porque son datos planos, estáticos.
A diferencia del ejemplo de Persona, donde sí tiene sentido crear instancias porque los datos varían (nombre, edad, etc.).
Para emplear los Companion Objects en Kotlin los mismos deben ser declarados dentro de la instancia de una clase de la siguiente forma:
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)
}
}
}En primera instancia, pareciera que emplear una estructura más compleja, ya que estamos anidando elementos, en este caso nuestra clase dentro de un Companion Object, y es verdad, pero gana en legibilidad, ya que es muy común que queramos definir un bloque importante como static, y no tenemos que estar colocando cada rato la palabra static para definir múltiples funciones, o parámetros como hacíamos en Java.
Con el englobamiento que hacemos con el Companion Object es más que suficiente para englobar todos nuestros métodos y propiedades estáticas de una sola vez; en Java tendríamos que hacer lo siguiente.
public static final String KEY = "key";Ahora, si tenemos varias estructuras y variables que queramos que sean static, entonces todo se nos complica bastante, porque tenemos que estar colocando static a cada rato, y si además tenemos métodos y atributos que no son estáticos, a la final lo que tenemos es una buena mezcla de varias cosas; cosa que no sucede con Kotlin al englobar en un bloque de código las propiedades y funciones que queramos sean estáticas mediante los Companion Objects.
En el ejemplo anterior, podemos apreciar que declaramos nuestro Companion Object dentro de la clase Persona, como principio fundamental, para crear y posteriormente usar un Companion Object el mismo debe de estar declarado dentro de una clase como hicimos anteriormente, que declaramos el Companion Object llamado Raza dentro de la clase madre Persona.
Accediendo a los métodos del Companion Object
Con el Companion Object ya declarado, creamos algunas propiedades como lo son edades y razas que son Arrays las cuales creamos método personalizados para cada ámbito en este ejemplo, es decir, creamos como ejemplo un método enAmerica() que retorna un Array con las razas nativas del continente Americano, que serían las de los latinos, los Americanos (américa del norte) y los canadienses.
Hacemos algo parecido con las edades, la cual creamos un método llamado adultoMayor() que retorna las edad en la que se considera que a partir de la misma, la persona es un adulto mayor.
Además creamos un texto mediante una variable llamada clasificacion que retorna su valor mediante el método printClasificacion.
Por último, podemos emplear un constructor definido como init en caso de que sea necesario.
Variaciones en la estructura del Companion Object para manejar los static
Podemos omitir el nombre del Companion Object de la declaración, quedando de la siguiente forma:
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*/
}Para poder emplear métodos o propiedades del Companion Object empleamos el siguiente esquema; tomando como ejemplo el código anterior, tenemos que:
println(Persona.enAmerica()[0]) println(Persona.printClasificacion()) println(Persona.adultoMayor()) }Accediendo a las propiedades del Companion Object
Para referenciar los métodos que vimos anteriormente; por supuesto, podemos referencias directamente las propiedades como hemos hecho en anteriores entradas, y esto es excelente, ya que no tenemos que estar creados los famosos métodos get y set para cada propiedad de nuestro objeto; con declarar la propiedad y su tipo es condición necesaria y suficiente:
Persona.edades
Persona.razas
Persona.clasificacionComo podemos ver, debemos emplear el nombre de la clase madre seguido del método o propiedad definido dentro del Companion Object, es el mismo enfoque que el realizado en Java al momento de referenciar a un método o propiedad de una clase estática en Java; es decir, primero referenciamos el nombre de la clase para luego referenciar el método o propiedad que del Companion Object sin la necesidad de crear un objeto de la clase.
Métodos de la clase madre del Companion Object
Podemos definir la estructura de nuestra clase como queramos, tomando como referencia la estructura que vimos en una sobre el uso de las clases.
Tenemos que nuestro código quedaría así:
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)
}
}
}Ejemplo: Base de datos
Un ejemplo muy clásico es una base de datos.
Los parámetros de conexión:
- Versión
- Nombre
- Configuración
Aquí tenemos un ejemplo sencillo de una clase con un companion object:
class BaseDeDatos {
companion object {
const val VERSION = 1
fun obtenerNombre(): String {
return "MiApp_DB"
}
}
}Para usarlo, no creamos una instancia:
println(BaseDeDatos.VERSION)
println(BaseDeDatos.obtenerNombre())Esto tiene todo el sentido del mundo, ya que esta información no va a variar durante la ejecución del programa.
A diferencia, por ejemplo, de los datos del usuario, que sí pueden cambiar (edad, nombre, etc.).
No deberían cambiar durante la ejecución del programa.
Por eso:
- Creamos la clase.
- Definimos un
companion objectsin nombre (porque no lo necesitamos). - Colocamos ahí la información estática.
Para acceder a ella, simplemente hacemos:
Database.versionDatabase.getName()
Sin crear instancias.
Tiene todo el sentido del mundo que estas propiedades sean constantes, ya que no varían durante la ejecución.
Factory: Constructores especiales
Ahora vamos con otro uso muy importante: los factory.
¿Qué es un Factory?
Un factory (fábrica) es una estructura que nos permite crear objetos de forma controlada.
Esto está muy ligado a la programación orientada a objetos, donde usamos clases para representar objetos del mundo real:
un usuario, un carro, una casa, una pizza.
Otro uso extremadamente común del companion object es como Factory, es decir, como una fábrica de objetos.
Ejemplo de la pizza
Imagina una pizzería:
- Hay un menú.
- Puedes pedir pizza de pepperoni, queso o margarita.
- No puedes pedir cosas que no existen.
Cuando tú pides una pizza:
- El proceso ya está optimizado.
- Los ingredientes ya están preparados.
- No se empieza desde cero cada vez.
Eso es exactamente lo que hace un factory.
Aplicado a programación
Supongamos que tienes usuarios:
- Usuario normal.
- Usuario administrador.
- Usuario invitado.
No tiene sentido repetir la misma lógica de validación en todos los módulos de la aplicación:
- Dashboard
- Registro
- API
- App móvil
Un factory centraliza toda esa lógica en un solo lugar.
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")
}
}Aquí el constructor es privado, lo que evita que se creen usuarios inválidos.
Toda la lógica queda centralizada en un solo lugar.
val guest = User.createGuest("Carlos")
val admin = User.createAdmin("Ana")
guest.showInfo()
admin.showInfo()Esto evita tener validaciones repetidas en distintos módulos de la aplicación.
Factory con Companion Object
En el ejemplo:
- El constructor es privado.
- Nadie puede crear usuarios directamente.
- Solo se pueden crear mediante los métodos del
companion object.
Esto evita errores como:
- Roles inválidos.
- Permisos incorrectos.
- Usuarios mal configurados.
Cada método del factory crea un usuario con una estructura válida y bien definida.
Ventajas del enfoque
- Toda la lógica está en un solo lugar.
- Evitas duplicar validaciones.
- El código es más limpio y mantenible.
- Es imposible crear objetos inválidos por accidente.
Exactamente igual que en la pizzería:
- No vas al supermercado cada vez que alguien pide una pizza.
Singleton + Factory
En este caso:
- El
companion objectactúa como factory. - La clase puede tener múltiples instancias.
- Pero la lógica de creación es única.
Luego, una vez creada la instancia, puedes acceder a los métodos normales de la clase.
Factory con Singleton: Notificaciones
Finalmente, Kotlin también ofrece la palabra clave object, que crea un singleton real.
A diferencia del companion object:
- No está dentro de una clase.
- Existe una sola instancia en toda la aplicación.
Esto es muy útil cuando quieres crear, por ejemplo:
- Un
NotificationFactory. - Un gestor global de servicios.
Aquí combinamos Factory + Singleton usando 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")
}
}
}Uso:
val notif = NotificacionFactory.crearNotificacion("EMAIL")
notif.enviar("¡Hola desde la Factory!")Aquí tenemos:
- Una instancia única (
object) - Capacidad de crear múltiples tipos de objetos
- Una interfaz común (
Notificacion) que garantiza un método compartido
Resumen
Aquí vemos un caso más avanzado:
- Una interfaz
Notification. - Implementaciones distintas: email, SMS, push.
- Un singleton que decide qué tipo de notificación crear.
Desde fuera, tú solo llamas:
createNotification(type)send(message)
No te importa cómo funciona internamente cada tipo de notificación.
Ventaja principal
La gran ventaja de todo este enfoque es que:
- Centralizas la lógica.
- Trabajas con estructuras claras.
- Evitas errores.
- El código es fácil de leer y mantener.
Resumen rápido
- Kotlin no tiene
static companion objectes su equivalente- Sirve para:
- Métodos y propiedades estáticas
- Singletons
- Factory Methods
- Centraliza lógica
- Evita errores y duplicación
- Hace el código más limpio y mantenible
Conclusión
companion object: estáticos dentro de una clase.object: singleton real.- Factory: creación controlada de objetos.
- Todo se puede combinar según el caso.
Espero que ahora quede mucho más claro cómo funciona cada uno y cuándo usar cada patrón.
Todos los ejemplos que vimos anteriormente son clases Singleton, que por definición son clases con una sola instancia en todo el ámbito de la aplicación; son ampliamente utilizadas para tener una sola conexión con la base de datos y evitar saturar o las famosas condiciones de carrera (cómo nos podría pasar en Android con las base de datos de SQLite), datos de usuarios y un largo etc.
Como puedes ver, los Companion Objects son muy útiles cuando trabajamos con estructuras que deben tener una instancia única, como dados de usuarios, o datos de conexión a la base de datos en Android.
Puedes consultar la documentación oficial en el siguiente enlace: Companion Objects
El siguiente paso, Los arrays y listas en Kotlin: Primeros pasos estas estructuras mutables e inmutables.