Menu
Coddy logo textTech

Singleton Pattern

Part of the Object Oriented Programming section of Coddy's Dart journey — lesson 90 of 110.

The Singleton pattern ensures that a class has only one instance throughout your entire application, and provides a global point of access to it. This is useful when exactly one object is needed to coordinate actions across the system - like a configuration manager, a database connection, or a logging service.

In Dart, you can implement a Singleton using a factory constructor combined with a private static instance:

class AppConfig {
  static final AppConfig _instance = AppConfig._internal();
  
  String appName = 'MyApp';
  String version = '1.0.0';
  
  // Private named constructor
  AppConfig._internal();
  
  // Factory constructor returns the same instance
  factory AppConfig() {
    return _instance;
  }
}

void main() {
  var config1 = AppConfig();
  var config2 = AppConfig();
  
  config1.appName = 'UpdatedApp';
  
  print(config2.appName);  // UpdatedApp
  print(identical(config1, config2));  // true
}

The private constructor _internal() prevents external instantiation. The static _instance field holds the single instance, created once when the class loads. Every call to AppConfig() returns this same instance, so changes made through one reference are visible everywhere.

Use Singletons sparingly - they introduce global state, which can make testing harder. They're best suited for truly shared resources that need consistent state across your application.

challenge icon

Challenge

Easy

Let's build a game settings manager using the Singleton pattern! You'll create a system that ensures only one instance of game settings exists throughout the application, so all parts of your game share the same configuration.

You'll organize your code into two files:

  • game_settings.dart: Create a GameSettings class that implements the Singleton pattern. Your settings manager should have:
    • A private static final instance that holds the single GameSettings object
    • A private named constructor to prevent external instantiation
    • A factory constructor that always returns the same instance
    • Three settings fields: difficulty (String, default: "Normal"), soundEnabled (bool, default: true), and playerName (String, default: "Player1")
    • A method displaySettings() that prints the current settings in this format: Settings: [playerName] | Difficulty: [difficulty] | Sound: [on/off] (print "on" if soundEnabled is true, "off" if false)
  • main.dart: Import your game settings and demonstrate that the Singleton works correctly:
    • Get the settings instance and call it settings1
    • Call displaySettings() to show the default values
    • Modify the settings: change playerName to "Hero", difficulty to "Hard", and soundEnabled to false
    • Get another reference to settings and call it settings2
    • Call displaySettings() on settings2 to prove it reflects the changes made through settings1
    • Print whether settings1 and settings2 are identical using: Same instance: [true/false]

This demonstrates the core benefit of Singletons - changes made anywhere in your application are immediately visible everywhere else, because there's only one instance!

Expected output:

Settings: Player1 | Difficulty: Normal | Sound: on
Settings: Hero | Difficulty: Hard | Sound: off
Same instance: true

Cheat sheet

The Singleton pattern ensures that a class has only one instance throughout your entire application, and provides a global point of access to it.

In Dart, implement a Singleton using a factory constructor combined with a private static instance:

class AppConfig {
  static final AppConfig _instance = AppConfig._internal();
  
  String appName = 'MyApp';
  String version = '1.0.0';
  
  // Private named constructor
  AppConfig._internal();
  
  // Factory constructor returns the same instance
  factory AppConfig() {
    return _instance;
  }
}

Key components:

  • static final _instance: Holds the single instance, created once when the class loads
  • _internal(): Private constructor prevents external instantiation
  • factory AppConfig(): Returns the same instance every time

Usage example:

var config1 = AppConfig();
var config2 = AppConfig();

config1.appName = 'UpdatedApp';

print(config2.appName);  // UpdatedApp
print(identical(config1, config2));  // true

Changes made through one reference are visible everywhere because all references point to the same instance.

Try it yourself

import 'game_settings.dart';

void main() {
  // TODO: Get the settings instance and call it settings1
  
  // TODO: Call displaySettings() to show the default values
  
  // TODO: Modify the settings:
  // - Change playerName to "Hero"
  // - Change difficulty to "Hard"
  // - Change soundEnabled to false
  
  // TODO: Get another reference to settings and call it settings2
  
  // TODO: Call displaySettings() on settings2 to prove it reflects the changes
  
  // TODO: Print whether settings1 and settings2 are identical
  // Format: Same instance: [true/false]
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming