Create Singleton Pattern in Kotlin Without Using object

The Singleton pattern is a design pattern that ensures a class has only one instance and provides a global point of access to that instance. While Kotlin provides a straightforward way to create singletons using the object keyword, there are scenarios where you might want to create a singleton without relying on it. For example, you might need lazy initialization, dependency injection, or more control over the lifecycle.

In this blog post, we will explore how to implement the Singleton pattern in Kotlin without using the object keyword.

1. Why Avoid the object Keyword?

Although object makes creating singletons in Kotlin effortless, there are some reasons to avoid it:

  • Lazy Initialization: The object keyword initializes the singleton immediately when the class is loaded, which might not be ideal in cases where initialization is expensive or should be deferred.

  • Customization: If you want more control over how the singleton is instantiated, such as injecting dependencies or adding additional logic, using object may not be flexible enough.

2. Implementing Singleton Without object

Here are some common approaches to creating a singleton manually:

a) Using a Private Constructor with a Companion Object

A common way to create a singleton is to use a private constructor and expose the single instance through a companion object.

class Singleton private constructor() {

    companion object {
        // Single instance of the class
        @Volatile
        private var instance: Singleton? = null

        fun getInstance(): Singleton {
            return instance ?: synchronized(this) {
                instance ?: Singleton().also { instance = it }
            }
        }
    }

    fun doSomething() {
        println("Singleton instance is working!")
    }
}

fun main() {
    val singleton = Singleton.getInstance()
    singleton.doSomething()
}

  1. The @Volatile annotation ensures that updates to a variable are visible to all threads immediately. Without this, one thread might see an outdated value while another thread is updating it.

Why it matters in Singleton? When multiple threads are involved, @Volatile prevents caching of the variable, ensuring all threads read the most recent value directly from memory.

Example:

@Volatile
private var instance: Singleton? = null

Effect:

  • Ensures visibility of changes across threads.

  • Prevents subtle bugs in multithreaded environments.

  1. The synchronized keyword ensures that only one thread at a time can execute a particular block of code. It is critical when you need to protect shared resources from concurrent access.

Why it matters in Singleton? Synchronization guarantees that only one thread creates the Singleton instance, avoiding issues like multiple initializations.

Example:

synchronized(this) {
    if (instance == null) {
        instance = Singleton()
    }
}

Effect:

  • Ensures thread safety.

  • Can have a performance overhead if overused, especially when used on frequently accessed methods.

Explanation:

  • The constructor is private to prevent direct instantiation.

  • The @Volatile annotation ensures that updates to the instance variable are visible to all threads.

  • The synchronized block ensures thread safety when creating the singleton instance.

b) Using Lazy Initialization

Lazy initialization is a powerful tool in Kotlin that can be used to create a thread-safe singleton with minimal boilerplate.

class Singleton private constructor() {

    companion object {
        val instance: Singleton by lazy { Singleton() }
    }

    fun doSomething() {
        println("Lazy Singleton instance is working!")
    }
}

fun main() {
    val singleton = Singleton.instance
    singleton.doSomething()
}

The by lazy keyword is a Kotlin-specific feature that allows lazy initialization of a variable. It means the variable is initialized only when it is accessed for the first time.

  • Why it matters in Singleton? It simplifies lazy initialization while providing thread safety by default.

Example:

val instance: Singleton by lazy { Singleton() }

Effect:

  • Automatically thread-safe (by default, lazy uses SYNCHRONIZED mode).

  • Reduces boilerplate code for creating and initializing singletons.

Explanation:

  • The lazy delegate ensures that the instance is created only when it is accessed for the first time.

  • The lazy block is thread-safe by default.

c) Using a Double-Checked Locking Pattern

If you want to implement the Singleton pattern manually with lazy initialization and thread safety, you can use the double-checked locking pattern.

class Singleton private constructor() {

    companion object {
        @Volatile
        private var instance: Singleton? = null

        fun getInstance(): Singleton {
            if (instance == null) {
                synchronized(this) {
                    if (instance == null) {
                        instance = Singleton()
                    }
                }
            }
            return instance!!
        }
    }

    fun doSomething() {
        println("Double-checked Singleton instance is working!")
    }
}

fun main() {
    val singleton = Singleton.getInstance()
    singleton.doSomething()
}

Explanation:

  • The if checks ensure that synchronization is only applied when the instance is null, reducing the performance overhead.

  • The synchronized block guarantees thread safety during initialization.

d) Injecting Dependencies in Singleton

When your singleton needs dependencies, you can initialize it using a factory method or dependency injection.

class Singleton private constructor(private val dependency: String) {

    companion object {
        @Volatile
        private var instance: Singleton? = null

        fun getInstance(dependency: String): Singleton {
            return instance ?: synchronized(this) {
                instance ?: Singleton(dependency).also { instance = it }
            }
        }
    }

    fun doSomething() {
        println("Singleton instance with dependency: $dependency")
    }
}

fun main() {
    val singleton = Singleton.getInstance("MyDependency")
    singleton.doSomething()
}

Explanation:

  • The factory method getInstance accepts parameters needed for initialization.

  • This approach allows injecting dependencies dynamically when creating the singleton.

3. Which Approach Should You Use?

  • Lazy Initialization: Use this when you need a simple and thread-safe singleton with minimal boilerplate.

  • Double-Checked Locking: Use this if you prefer more explicit control over how the singleton is initialized.

  • Factory Method with Dependency Injection: Use this when your singleton requires parameters or dependencies during construction.

4. Final Thoughts

The object keyword in Kotlin is a convenient way to create singletons, but it’s not the only option. By understanding and implementing manual singleton patterns, you gain more control over initialization, thread safety, and dependency injection. These approaches can be especially useful in complex projects where singletons need to adhere to specific requirements.

Akshay Nandwana
Founder AndroidEngineers

You can connect with me on:


Book 1:1 Session here
Click Here

Join our upcoming classes
https://www.androidengineers.in/courses

Love from our past students

Excellent list of questions really helped me to coverup all the topics before interview.

Saiteja Janjirala

10th Oct, 2024

I had an exceptional experience with the 1:1 mentorship session. Akshay was incredibly friendly and provided invaluable guidance on focusing on long-term goals. They also gave great interview tips, including a thorough resume review. Additionally, the discussion on Data Structures and Algorithms (DSA) was insightful and practical. Highly recommended for anyone looking to advance their career!

Nayab khan

11th Sep, 2024

Cleared my major points for what I am missing in the resume and also suggested what I can work on for further growth in the career.

Ketan Chaurasiya

7th Aug, 2024

What impressed me most was his personalized approach and practical tips that I could immediately apply. Akshay’s guidance not only improved my technical skills but also boosted my confidence in navigating my career path. His insights and encouragement have been a game-changer for me. I highly recommend Akshay’s mentorship to anyone looking to advance their Android development career.

Hardik Kubavat

5th Aug, 2024

2025© Made with   by Android Engineers.