Dart 20 🎯 Factory Constructors and Singleton Design Patterns
A factory constructor is a constructor that does not create a new instance directly. Instead, it returns an instance, which may be newly created, cached, a subtype, or produced by another class. The factory keyword marks the constructor, and the body returns an object that matches the class type. This flexibility is what makes factory constructors the mechanism for singletons, caches, deserialization, and the named-constructor patterns that cannot be expressed with a generative constructor. A generative constructor must initialize the fields of a fresh instance; a factory constructor has no such restriction.
The singleton pattern is the canonical use of the factory constructor. A class that should have exactly one instance exposes a private generative constructor and a static field that holds the instance. The public factory constructor returns that instance, creating it on first use if necessary. This gives the class a single point of access while keeping the constructor private and the instance lazily initialized. The pattern appears throughout Dart and Flutter: services, caches, configuration holders, and clients that manage a shared resource.
This chapter covers the factory keyword, factory constructors that return cached instances, factory constructors that return subtypes, factory constructors for deserialization, the singleton pattern, the lazy singleton, the eager singleton, and the mistakes that break the singleton guarantee.
Key point: A factory constructor returns an instance; it does not have to create a new one. Use it for singletons, caches, deserialization, and returning subtypes. A singleton has a private constructor and a static field that holds the single instance. The lazy singleton creates the instance on first access; the eager singleton creates it at class initialization. A factory constructor cannot access this and cannot be const unless it returns a const instance.
Why factory constructors exist
The cache problem. Some classes should return an existing instance when the same arguments are used. A color palette, a font cache, or a connection pool can return a shared instance instead of allocating a new one. A generative constructor cannot do this; it always creates a new object. A factory constructor can.
The subtype problem. Some constructors should return a subtype based on the arguments. A Shape factory that returns a Circle or a Square depending on the type string is a factory constructor. The caller sees a Shape, and the actual type is one of the subtypes.
The deserialization problem. A class that is constructed from JSON, a database row, or a network response often needs to validate the input and return either an instance or an error. A factory constructor can perform the validation and return an instance, a cached instance, or a subtype.
The singleton problem. Some classes should have exactly one instance: a configuration holder, a service registry, a logger. The singleton pattern enforces this. The private constructor prevents external instantiation, and the factory constructor returns the single instance.
The this problem. A generative constructor has access to this and initializes the instance’s fields. A factory constructor does not have this, because it does not create the instance directly. It can call other constructors, return cached instances, or delegate to another class. This is the mechanism that makes the flexible patterns possible.
a. The factory keyword
A factory constructor is declared with the factory keyword before the constructor name. Its body returns an instance of the class.
class Point {
final double x;
final double y;
Point(this.x, this.y);
factory Point.origin() {
return Point(0, 0);
}
}
The Point.origin() factory returns a new Point created by the generative constructor. This is a factory constructor, but it does not do anything a generative constructor could not do. It becomes useful when the factory returns something other than a fresh instance.
A factory constructor cannot access this. It has no initializer list, and it must return an instance of the class (or a subtype) using the return statement.
factory Point.fromJson(Map<String, dynamic> json) {
return Point(json['x'] as double, json['y'] as double);
}
The factory receives the parameters, performs whatever logic is needed, and returns an instance.
A factory constructor can be const only if it returns a const instance:
class Point {
final double x;
final double y;
const Point(this.x, this.y);
factory Point.origin() = Point.zero; // redirecting factory
static const Point zero = Point(0, 0);
}
The redirecting factory syntax (= Target) forwards to another constructor. It is a shorthand for a factory whose body is a single return of another constructor call.
b. Factory constructors that return cached instances
A factory constructor can return a cached instance instead of creating a new one.
class Color {
final int r;
final int g;
final int b;
Color._(this.r, this.g, this.b);
static final Map<String, Color> _cache = {};
factory Color.rgb(int r, int g, int b) {
final key = '$r,$g,$b';
return _cache.putIfAbsent(key, () => Color._(r, g, b));
}
}
The private generative constructor Color._ creates the instance. The factory checks the cache and returns the existing instance if the same RGB values have been requested before. The result is that Color.rgb(255, 0, 0) always returns the same instance.
var red1 = Color.rgb(255, 0, 0);
var red2 = Color.rgb(255, 0, 0);
print(identical(red1, red2)); // true
The cache is keyed by the values, so identical requests share the instance. This is the pattern for flyweight objects: a large number of values that can be represented by a small number of shared instances.
The cache must be cleared if the instances should be eligible for garbage collection. For a small number of fixed values, the cache is permanent and the instances live for the program’s lifetime.
c. Factory constructors that return subtypes
A factory constructor can return a subtype based on the arguments.
abstract class Shape {
double area();
factory Shape.fromType(String type, double size) {
switch (type) {
case 'circle':
return Circle(size);
case 'square':
return Square(size);
default:
throw ArgumentError('Unknown shape: $type');
}
}
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
@override
double area() => 3.14159 * radius * radius;
}
class Square implements Shape {
final double side;
Square(this.side);
@override
double area() => side * side;
}
The Shape.fromType factory returns a Circle or a Square depending on the type string. The caller sees a Shape, and the actual type is one of the subtypes.
var shape = Shape.fromType('circle', 5);
print(shape.area()); // 78.53975
The factory is the only way to construct a Shape, because the class is abstract. The concrete types are created by the factory and returned as the abstract type.
This pattern is common in deserialization, plugin systems, and any case where the concrete type depends on the input.
d. The singleton pattern
A singleton is a class with exactly one instance. The pattern uses a private constructor to prevent external instantiation and a factory constructor to return the single instance.
class Logger {
static final Logger _instance = Logger._internal();
factory Logger() {
return _instance;
}
Logger._internal();
void log(String message) {
print('[LOG] $message');
}
}
The private generative constructor Logger._internal() creates the instance. The static field _instance holds it. The factory constructor Logger() returns the static field. External code calls Logger() and receives the same instance every time.
var a = Logger();
var b = Logger();
print(identical(a, b)); // true
The singleton is eager: the instance is created when the class is first loaded. The static final field is initialized once, and the factory returns it.
The pattern has three parts:
| Part | Purpose |
|---|---|
| Private constructor | Prevents external instantiation |
| Static field | Holds the single instance |
| Factory constructor | Returns the single instance |
The factory constructor is what makes Logger() work while Logger._internal() remains private. The call site looks like a normal constructor call.
e. The lazy singleton
The eager singleton creates the instance when the class is loaded. The lazy singleton creates it on first access.
class Config {
static Config? _instance;
factory Config() {
return _instance ??= Config._internal();
}
Config._internal();
String get host => 'localhost';
}
The static field is nullable and starts as null. The factory checks whether it is null and creates the instance on the first call. Subsequent calls return the existing instance.
var a = Config();
var b = Config();
print(identical(a, b)); // true
The lazy version is useful when the instance’s construction is expensive and might not be needed. The eager version is simpler and is the right choice when the instance is always needed.
The ??= operator is the idiomatic way to write the lazy check. It assigns the right side to the left only if the left is null, and it returns the value.
A lazy singleton with a non-nullable static field uses the late keyword:
class Config {
static late final Config _instance = Config._internal();
factory Config() => _instance;
Config._internal();
}
The late final field is initialized on first access. The factory returns it. The late keyword defers the initialization to the first read.
f. Factory constructors and const
A factory constructor cannot be const unless it returns a const instance. The const constructor and the factory constructor are different mechanisms.
class Point {
final double x;
final double y;
const Point(this.x, this.y);
// Not allowed: a factory cannot be const unless it returns a const instance
// const factory Point.origin() => Point(0, 0);
}
The correct way to provide a const alternative is a static field:
class Point {
final double x;
final double y;
const Point(this.x, this.y);
static const Point origin = Point(0, 0);
}
// Usage: Point.origin
The static const field is a const instance that is created at compile time and canonicalized. It serves the same purpose as a const factory would, without the restriction.
The redirecting factory syntax can forward to a const constructor, but the factory itself is not const:
class Point {
final double x;
final double y;
const Point(this.x, this.y);
factory Point.origin() = Point.zero;
static const Point zero = Point(0, 0);
}
The factory Point.origin() = Point.zero forwards to the static field zero. The syntax is a redirecting factory, and it works because the target is a static field that holds a const instance.
Complete Example Session
// ============================================
// PART 1: BASIC FACTORY
// ============================================
class Point {
final double x;
final double y;
Point(this.x, this.y);
factory Point.origin() {
return Point(0, 0);
}
}
void main() {
var origin = Point.origin();
print('${origin.x}, ${origin.y}'); // 0.0, 0.0
}
// ============================================
// PART 2: FACTORY FROM JSON
// ============================================
class Point {
final double x;
final double y;
Point(this.x, this.y);
factory Point.fromJson(Map<String, dynamic> json) {
return Point(json['x'] as double, json['y'] as double);
}
}
void main() {
var p = Point.fromJson({'x': 1.0, 'y': 2.0});
print('${p.x}, ${p.y}'); // 1.0, 2.0
}
// ============================================
// PART 3: CACHED INSTANCES
// ============================================
class Color {
final int r, g, b;
Color._(this.r, this.g, this.b);
static final Map<String, Color> _cache = {};
factory Color.rgb(int r, int g, int b) {
final key = '$r,$g,$b';
return _cache.putIfAbsent(key, () => Color._(r, g, b));
}
}
void main() {
var red1 = Color.rgb(255, 0, 0);
var red2 = Color.rgb(255, 0, 0);
print(identical(red1, red2)); // true
}
// ============================================
// PART 4: FACTORY RETURNING A SUBTYPE
// ============================================
abstract class Shape {
double area();
factory Shape.fromType(String type, double size) {
switch (type) {
case 'circle': return Circle(size);
case 'square': return Square(size);
default: throw ArgumentError('Unknown shape: $type');
}
}
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
double area() => 3.14159 * radius * radius;
}
class Square implements Shape {
final double side;
Square(this.side);
double area() => side * side;
}
// ============================================
// PART 5: USING THE FACTORY
// ============================================
void main() {
var shape = Shape.fromType('circle', 5);
print(shape.area()); // 78.53975
}
// ============================================
// PART 6: EAGER SINGLETON
// ============================================
class Logger {
static final Logger _instance = Logger._internal();
factory Logger() {
return _instance;
}
Logger._internal();
void log(String message) => print('[LOG] $message');
}
void main() {
var a = Logger();
var b = Logger();
print(identical(a, b)); // true
}
// ============================================
// PART 7: LAZY SINGLETON
// ============================================
class Config {
static Config? _instance;
factory Config() {
return _instance ??= Config._internal();
}
Config._internal();
String get host => 'localhost';
}
// ============================================
// PART 8: LAZY SINGLETON WITH LATE
// ============================================
class Settings {
static late final Settings _instance = Settings._internal();
factory Settings() => _instance;
Settings._internal();
}
// ============================================
// PART 9: REDIRECTING FACTORY
// ============================================
class Point {
final double x;
final double y;
const Point(this.x, this.y);
factory Point.origin() = Point.zero;
static const Point zero = Point(0, 0);
}
void main() {
var origin = Point.origin();
print(origin.x); // 0.0
}
// ============================================
// PART 10: FACTORY WITH VALIDATION
// ============================================
class Email {
final String address;
Email._(this.address);
factory Email(String address) {
if (!address.contains('@')) {
throw ArgumentError('Invalid email: $address');
}
return Email._(address);
}
}
void main() {
var email = Email('alice@example.com');
print(email.address); // alice@example.com
}
These ten parts cover the basic factory, a factory from JSON, cached instances, a factory returning a subtype, using the factory, the eager singleton, the lazy singleton, the lazy singleton with late, a redirecting factory, and a factory with validation.
Quick Reference
Factory Constructor Forms
| Form | Syntax |
|---|---|
| Standard | factory Class.name(...) { return ...; } |
| Redirecting | factory Class.name(...) = Target; |
| Const | const factory Class.name(...) = ConstTarget; |
Factory vs Generative
| Aspect | Generative | Factory |
|---|---|---|
| Creates instance | Always | May return cached, subtype |
Access to this | Yes | No |
| Initializer list | Yes | No |
| Return statement | Implicit | Required |
| Const | Possible | Only with const return |
Singleton Pattern
| Part | Purpose |
|---|---|
| Private constructor | Prevent external instantiation |
| Static field | Hold the single instance |
| Factory constructor | Return the single instance |
Singleton Variants
| Variant | When created | Example |
|---|---|---|
| Eager | Class load | static final _instance = ... |
| Lazy | First access | static T? _instance + ??= |
| Late final | First access | static late final _instance = ... |
Common Factory Uses
| Use case | Why a factory |
|---|---|
| Singleton | Return the shared instance |
| Cache | Return an existing instance |
| Subtype | Return a concrete subtype |
| Deserialization | Validate and return |
| Validation | Throw on invalid input |
Best Practices
✅ Do This:
// Use a factory for a singleton
class Logger {
static final Logger _instance = Logger._internal();
factory Logger() => _instance;
Logger._internal();
}
// Use a factory for caching
factory Color.rgb(int r, int g, int b) {
return _cache.putIfAbsent(key, () => Color._(r, g, b));
}
// Use a factory for subtypes
factory Shape.fromType(String type) { ... }
// Use a redirecting factory for shorthands
factory Point.origin() = Point.zero;
// Use a static const for const alternatives
static const Point origin = Point(0, 0);
❌ Don’t Do This:
// Use a generative constructor for a singleton
Logger() { } // ❌ creates a new instance each time
// Make the singleton constructor public
Logger(); // ❌ external code can create instances
// Use a factory when a generative works
factory Point(double x, double y) { return Point._(x, y); } // ❌ unnecessary
// Forget to make the generative constructor private
class Logger {
Logger(); // ❌ should be Logger._internal()
}
// Return a new instance from a singleton factory
factory Logger() { return Logger._internal(); } // ❌ not a singleton
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Multiple instances | Public constructor | Make it private |
this not available | Used in a factory | Use the parameters |
| Const factory error | Returns non-const | Use a static const field |
| Cache grows unbounded | No eviction | Clear or bound the cache |
| Lazy singleton race | Not thread-safe in isolates | Accept or synchronize |
| Redirecting factory target | Target is not a constructor | Use a redirecting syntax or a body |
Real-World Examples
1. Singleton Logger
class Logger {
static final Logger _instance = Logger._internal();
factory Logger() => _instance;
Logger._internal();
}
2. Lazy Singleton
class Config {
static Config? _instance;
factory Config() => _instance ??= Config._internal();
Config._internal();
}
3. Cached Color
factory Color.rgb(int r, int g, int b) {
return _cache.putIfAbsent('$r,$g,$b', () => Color._(r, g, b));
}
4. Subtype Factory
factory Shape.fromType(String type, double size) { ... }
5. Deserialization
factory User.fromJson(Map<String, dynamic> json) {
return User(json['name'] as String, json['age'] as int);
}
6. Validation Factory
factory Email(String address) {
if (!address.contains('@')) throw ArgumentError();
return Email._(address);
}
7. Redirecting Factory
factory Point.origin() = Point.zero;
8. Singleton Service
class ApiClient {
static final ApiClient _instance = ApiClient._internal();
factory ApiClient() => _instance;
ApiClient._internal();
}
9. Connection Pool
factory Pool() => _sharedPool;
static final Pool _sharedPool = Pool._internal();
10. Configuration Holder
class AppConfig {
static late final AppConfig _instance = AppConfig._internal();
factory AppConfig() => _instance;
AppConfig._internal();
}
Visual
Factory vs Generative
┌──────────────────────────────────────────────────────────────┐
│ GENERATIVE: │
│ Point(this.x, this.y); │
│ └── Always creates a new instance │
│ └── Has access to `this` │
│ └── Initializes fields directly │
│ │
│ FACTORY: │
│ factory Point.origin() { return Point(0, 0); } │
│ └── Returns an instance │
│ └── No access to `this` │
│ └── Can return cached, subtype, or new │
└──────────────────────────────────────────────────────────────┘
Singleton Pattern
┌──────────────────────────────────────────────────────────────┐
│ class Logger { │
│ static final Logger _instance = Logger._internal(); │
│ │ │
│ │ ← holds the single instance │
│ │ │
│ factory Logger() => _instance; │
│ │ │
│ │ ← returns the single instance │
│ │ │
│ Logger._internal(); │
│ │ │
│ │ ← private, prevents external instantiation │
│ } │
│ │
│ var a = Logger(); ──┐ │
│ var b = Logger(); ──┼──▶ same instance │
│ var c = Logger(); ──┘ │
└──────────────────────────────────────────────────────────────┘
Lazy vs Eager Singleton
┌──────────────────────────────────────────────────────────────┐
│ EAGER: │
│ static final Logger _instance = Logger._internal(); │
│ └── Created when the class is loaded │
│ │
│ LAZY: │
│ static Logger? _instance; │
│ factory Logger() => _instance ??= Logger._internal(); │
│ └── Created on first call │
│ │
│ LATE FINAL: │
│ static late final Logger _instance = Logger._internal(); │
│ factory Logger() => _instance; │
│ └── Created on first access, non-nullable │
└──────────────────────────────────────────────────────────────┘
Factory Returning a Subtype
┌──────────────────────────────────────────────────────────────┐
│ Shape.fromType('circle', 5) │
│ │ │
│ ▼ │
│ factory Shape.fromType(String type, double size) { │
│ switch (type) { │
│ case 'circle': return Circle(size); │
│ case 'square': return Square(size); │
│ } │
│ } │
│ │
│ Caller sees Shape; actual type is Circle. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Factory keyword | factory Class.name(...) |
| Creates instance | Not necessarily |
Access to this | No |
| Return statement | Required |
| Const factory | Only with const return |
| Redirecting factory | factory X() = Target; |
| Singleton | Private constructor + static field + factory |
| Eager singleton | static final _instance = ... |
| Lazy singleton | static T? _instance + ??= |
| Late final singleton | static late final _instance = ... |
| Cache factory | putIfAbsent |
| Subtype factory | Switch on input, return subtype |
Key takeaways:
- A factory constructor returns an instance; it does not have to create one. It can return a cached instance, a subtype, or an instance created by another class. This is what makes it more flexible than a generative constructor.
- A factory constructor has no
thisand no initializer list. It receives the parameters, performs the logic, and returns an instance with thereturnstatement. - The singleton pattern uses three parts: a private constructor, a static field, and a factory constructor. The private constructor prevents external instantiation, the static field holds the single instance, and the factory returns it.
- The lazy singleton creates the instance on first access. The
static T? _instancefield with??=is the standard pattern. Thestatic late final _instance = ...is the non-nullable version. - The eager singleton creates the instance at class load. The
static final _instance = ...field is initialized once, and the factory returns it. This is simpler and is the right choice when the instance is always needed. - A cache factory returns an existing instance for repeated arguments. The
putIfAbsentpattern keys the cache by the arguments and returns the shared instance. - A subtype factory returns a concrete type based on the input. The caller sees the abstract type, and the actual type is one of the subtypes. This is common in deserialization and plugin systems.
- A factory cannot be
constunless it returns aconstinstance. The alternative is astatic constfield that holds theconstinstance.
Remember: The factory constructor is the mechanism that separates the construction interface from the construction logic. It allows a class to control how instances are created, whether they are cached, and what concrete type is returned. The singleton pattern is the most common use: a private constructor, a static field, and a factory that returns the single instance. The pattern appears throughout Dart and Flutter for services, caches, configuration holders, and clients that manage shared resources. Understanding the factory constructor is understanding how Dart classes can control their own instantiation.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!