LogoPortfolio
Home
Projects
Articles
Certificates
More
Contact
Back to Articles
KotlinObjectsProgrammingAndroid

Companion Objects vs. Singletons in Kotlin: A No-Nonsense Guide with a Cosmic Twist

Published on June 1, 2025

Companion Objects vs. Singletons in Kotlin: A No-Nonsense Guide with a Cosmic Twist

Kotlin is packed with features that make coding cleaner and more intuitive. Two concepts that often spark confusion — even among seasoned developers — are companion objects and singletons. Let’s demystify them with a fresh perspective, zero jargon, and a quirky example you won’t find anywhere else.

Companion Objects: Your Class’s Personal Assistant

A companion object is like a loyal sidekick tied to a specific class. It can access the class’s private members (yes, even the secrets!) and is ideal for housing methods or properties that belong to the class but don’t need a separate instance.

How to Spot One

Declare it inside a class with the companion keyword:

class Spaceship(private val engineSerial: String) {  
    // Companion object  
    companion object BlackBox {  
        fun logLaunch(serial: String) {  
            println("Spaceship with engine $serial launched! 🚀")  
        }  
    }  

fun launch() {  
        BlackBox.logLaunch(engineSerial) // Access private engineSerial  
    }  
}

Here, BlackBox is a companion object inside the Spaceship class. It logs launches using the private engineSerial, something only a companion can do.

Why Use a Companion?

  • Access Private Data: It’s the only “object” that can peek into a class’s private properties.

  • Class-Specific Logic: Perfect for factory methods (Spaceship.create()) or constants tied to the class.

  • No Duplicate Instances: There’s only one companion per class, so no memory bloat.

Singletons: The Global Supervisors

A singleton in Kotlin is a globally accessible, single instance of an object. Declare it using object (not inside a class!), and it lives independently.

How to Spot One

object CentralComputer {  
    private val spaceships = mutableListOf<String>()  
    
    fun trackSpaceship(serial: String) {  
        spaceships.add(serial)  
        println("Now tracking ${spaceships.size} spaceships.")  
    }  
}

The CentralComputer singleton tracks all spaceships across your app. Any class can call CentralComputer.trackSpaceship(...), and there’s only one instance managing the list.

Why Use a Singleton?

  • Global Access: Need a shared resource (like a database connection)? Singletons are your friend.

  • Lazy Initialization: Created only when first used, saving memory.

  • Stateless Services: Great for logging, analytics, or hardware control.

Companion vs. Singleton: The Cosmic Showdown

Aspect               | Companion Object                          | Singleton  
---------------------|-------------------------------------------|-----------------------------------------  
Scope                | Tied to a class (like Spaceship.BlackBox) | Standalone (CentralComputer)  
Private Access       | Can access private members of its class   | Can’t unless nested inside the class  
Initialization       | Created when the class is loaded          | Created lazily on first use  
Use Case             | Class-specific factories, constants       | App-wide resources, shared state  

When to Use Which?

  1. Companion Objects Are Better When…
  • You need a factory method for a class (e.g., Spaceship.fromConfig()).

  • Storing constants specific to a class (e.g., Spaceship.MAX_SPEED).

  • Accessing private properties of the class (like logging internal data).

2. Singletons Shine When…

  • Managing app-wide resources (e.g., a network client or database).

  • You need a single source of truth (e.g., a user session manager).

  • You want lazy initialization to optimize performance.

A Unique Example: Spaceships and Cosmic Control

Let’s combine both concepts in a real-world scenario:

// Singleton: Manages all spaceships in the universe  
object UniverseRegistry {  
    private val allShips = mutableListOf<Spaceship>()  

    fun register(ship: Spaceship) {  
        allShips.add(ship)  
        println("Universe now has ${allShips.size} spaceships!")  
    }  
}  

class Spaceship(private val engineSerial: String) {  
    init {  
        UniverseRegistry.register(this) // Singleton in action!  
    }  
    // Companion: Handles launch logic for individual ships  
    companion object BlackBox {  
        fun launch(ship: Spaceship) {  
            println("Engine ${ship.engineSerial} firing!")  
        }  
    }  
    fun launch() {  
        BlackBox.launch(this)  
    }  
}  
// Example Usage  
val ship1 = Spaceship("X-101")  
val ship2 = Spaceship("X-202")  
ship1.launch() // Output: Engine X-101 firing!

Here, UniverseRegistry (singleton) tracks all ships globally, while BlackBox (companion) handles ship-specific launches.

Final Thoughts

  • Companion objects are your class’s trusted ally, handling intimate tasks.

  • Singletons are the overseers, managing cross-cutting concerns.

Next time you’re coding in Kotlin, ask: “Does this belong to a single class, or is it a global service?” The answer will guide you to the right tool.

About

Professional portfolio showcasing my work, articles, and achievements.

Quick Links

  • Projects
  • Articles
  • Certificates
  • Contact

Connect

GitHubGitHubLinkedInMediumMedium

Subscribe

© 2026 Portfolio. All rights reserved.