.png&w=3840&q=75)
.png)
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()
}
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.
Explanation:
The constructor is private to prevent direct instantiation.
The
@Volatile
annotation ensures that updates to theinstance
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
Get the latest Android development articles delivered to your inbox.