| |

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:

PartPurpose
Private constructorPrevents external instantiation
Static fieldHolds the single instance
Factory constructorReturns 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

FormSyntax
Standardfactory Class.name(...) { return ...; }
Redirectingfactory Class.name(...) = Target;
Constconst factory Class.name(...) = ConstTarget;

Factory vs Generative

AspectGenerativeFactory
Creates instanceAlwaysMay return cached, subtype
Access to thisYesNo
Initializer listYesNo
Return statementImplicitRequired
ConstPossibleOnly with const return

Singleton Pattern

PartPurpose
Private constructorPrevent external instantiation
Static fieldHold the single instance
Factory constructorReturn the single instance

Singleton Variants

VariantWhen createdExample
EagerClass loadstatic final _instance = ...
LazyFirst accessstatic T? _instance + ??=
Late finalFirst accessstatic late final _instance = ...

Common Factory Uses

Use caseWhy a factory
SingletonReturn the shared instance
CacheReturn an existing instance
SubtypeReturn a concrete subtype
DeserializationValidate and return
ValidationThrow 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

PitfallWhy It HappensFix
Multiple instancesPublic constructorMake it private
this not availableUsed in a factoryUse the parameters
Const factory errorReturns non-constUse a static const field
Cache grows unboundedNo evictionClear or bound the cache
Lazy singleton raceNot thread-safe in isolatesAccept or synchronize
Redirecting factory targetTarget is not a constructorUse 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

ItemValue
Factory keywordfactory Class.name(...)
Creates instanceNot necessarily
Access to thisNo
Return statementRequired
Const factoryOnly with const return
Redirecting factoryfactory X() = Target;
SingletonPrivate constructor + static field + factory
Eager singletonstatic final _instance = ...
Lazy singletonstatic T? _instance + ??=
Late final singletonstatic late final _instance = ...
Cache factoryputIfAbsent
Subtype factorySwitch 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 this and no initializer list. It receives the parameters, performs the logic, and returns an instance with the return statement.
  • 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? _instance field with ??= is the standard pattern. The static 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 putIfAbsent pattern 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 const unless it returns a const instance. The alternative is a static const field that holds the const instance.

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!