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.
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.
object
Here are some common approaches to creating a singleton manually:
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()
}
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.
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.
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.
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.
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.
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()
}
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.
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()
}
The factory method getInstance
accepts parameters needed for initialization.
This approach allows injecting dependencies dynamically when creating the singleton.
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.
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:
Join our upcoming classes
https://www.androidengineers.in/courses