Introduction
The Singleton pattern in Dart ensures that a class has only one instance and provides a global point of access to it. This pattern helps reduce redundancy by allowing you to access a single instance of a class throughout your entire application. In Flutter, it’s commonly used to manage shared resources like network connections, authentication services, or data storage.
In this tutorial, we’ll break down the Singleton pattern into two sections with practical, real-world examples.
Section 1: Understanding Singletons in Dart
The Singleton pattern in Dart allows you to maintain only one instance of a class throughout your application. This can be useful when managing shared resources or maintaining a global state.
Implementing a Singleton
To implement a singleton in Dart, you define a class with a private constructor and a static instance variable. A factory constructor returns the instance of the class, ensuring only one instance is created.
Here's an example of implementing a Singleton pattern for managing user authentication:
class AuthService {
static AuthService? _instance;
// Private constructor
const AuthService._();
// Factory constructor ensures only one instance
factory AuthService() =>_instance ??= const AuthService._();
// Simulate user login
void login(String username, String password) {
print('User $username logged in successfully!');
}
// Simulate user logout
void logout() {
print('User logged out');
}
}
Explanation
Private Constructor: The AuthService._() constructor is private to prevent external instantiation.
Static Instance: The static variable _instance holds the single instance of the class.
Factory Constructor: The factory constructor checks if an instance exists. If not, it creates one and returns it.
Using the Singleton
Now, let’s see how the AuthService singleton is used to manage user login:
void main() {
var auth1 = AuthService();
var auth2 = AuthService();
print(identical(auth1, auth2)); // Output: true
auth1.login('user1', 'password123'); // Output: User user1 logged in successfully!
auth2.logout(); // Output: User logged out
}
In this example, both auth1 and auth2 refer to the same instance of AuthService. This ensures that the user login and logout state is shared across the application, and only one authentication service is used.
Section 2: Singleton Registrar - Managing Multiple Instances with a Map
In some applications, you may need to manage multiple singleton instances of different classes. Instead of implementing a singleton for each class individually, you can create a Singleton Registrar that uses a Map to store and retrieve instances of different classes. This allows you to centralize the management of singleton instances and ensure that only one instance of each class exists.
Implementing the Singleton Registrar
Here’s how you can implement a Singleton Registrar that uses a Map to store instances and ensures that only one instance of each class is created.
class Registrar {
// A Map to store instances of singleton classes by their type
static final Map<Type, dynamic> _instances = {};
// Registers an instance of a class. If the instance already exists, it returns false
static bool register<T>(T instance) {
if (_instances.containsKey(T)) {
return false; // Instance already exists
}
_instances[T] = instance;
return true; // Successfully registered the instance
}
// Retrieves the instance of a class
static T get<T>() {
if (!_instances.containsKey(T)) {
throw Exception('Instance of type $T is not registered');
}
return _instances[T] as T;
}
}
Explanation
- Map of Instances: The _instances map holds the singleton instances of different classes, with the class Type as the key and the instance as the value.
- register: This method attempts to register an instance of a class. If an instance of the class already exists in the map, it returns false to indicate that registration failed. If the instance does not exist, it stores the new instance in the map and returns true.
- get: This method retrieves the registered instance of the class. If the instance is not registered, it throws an exception to inform the caller.
Example Singleton Classes
Here’s how you can define your singleton classes, such as AuthService and SettingsManager:
class AuthService {
void login(String username) {
print('User $username logged in');
}
}
class SettingsManager {
void changeTheme(String theme) {
print('Theme changed to $theme');
}
}
Using the Singleton Registrar
Now, let’s see how you can use the Registrar to manage singleton instances:
void main() {
var authService = AuthService();
var settingsManager = SettingsManager();
// Registering instances
bool authRegistered = Registrar.register(authService);
bool settingsRegistered = Registrar.register(settingsManager);
print(authRegistered); // Output: true (first time registration)
print(settingsRegistered); // Output: true (first time registration)
// Attempting to register the same instance again
bool authRegisteredAgain = Registrar.register(authService);
bool settingsRegisteredAgain = Registrar.register(settingsManager);
print(authRegisteredAgain); // Output: false (already registered)
print(settingsRegisteredAgain); // Output: false (already registered)
// Retrieving instances
var auth1 = Registrar.get<AuthService>();
var settings1 = Registrar.get<SettingsManager>();
// Using the retrieved instances
auth1.login('user1'); // Output: User user1 logged in
settings1.changeTheme('Dark'); // Output: Theme changed to Dark
}
Explanation of the Example
- Registering Instances: We create instances of AuthService and SettingsManager and register them using Registrar.register(). The first time registration returns true, indicating the instance was successfully registered.
- Preventing Duplicate Registrations: When we try to register the same instance again, the register() method returns false, preventing multiple instances of the same class.
- Retrieving Instances: We use Registrar.get() to retrieve the registered instances. If an instance exists, it is returned; otherwise, an exception is thrown.
- Using Instances: After retrieving the instances, we can call methods like login() or changeTheme() on the instances, ensuring that we’re working with the same object throughout the app.
Benefits of the Singleton Registrar
- Centralized Management: You can manage all singleton instances in one place, which makes it easier to track and maintain them.
- Preventing Duplicates: The registrar ensures that no duplicate instances of a class can be created, which is essential for managing global resources.
- Flexibility: The Registrar can be easily extended to manage other types of shared resources by adding more classes to the register() method.
Conclusion
The Singleton Registrar pattern provides a powerful way to manage multiple singleton instances in a centralized manner. By using a Map to store instances, you can ensure that only one instance of each class is created, and you can retrieve them efficiently throughout your application. This approach makes it easier to manage global resources like authentication services, settings, or network connections.
Thank you for reading! 🤗🤗 Happy Coding 👨🏿💻
Top comments (0)