Skip to main content

Command Palette

Search for a command to run...

Dart Private Constructors and Factory Methods: Control Object Creation Like a Pro

Updated
5 min read
Dart Private Constructors and Factory Methods: Control Object Creation Like a Pro

When Classes Start Misbehaving

At some point in every Dart project, you run into a situation where your classes are being used in ways you never intended. Someone creates an object directly when there should only ever be one. Someone calls an internal helper from outside the file. Someone instantiates a utility class that was never meant to be instantiated.

The language gives you tools to prevent all of this — private constructors, factory constructors, and private functions. They're not just syntax features; they're the building blocks of intentional API design.


Constructors: The Starting Point

A constructor is how you create an object from a class.

class ToyBox {
  ToyBox() {
    print('A new ToyBox is created!');
  }
}

void main() {
  var box = ToyBox(); // Creates a new instance
}

This is fine for most cases. But sometimes, you don't want every caller to freely spin up new instances. That's where private constructors come in.


Private Constructors: Controlling How Objects Are Created

A private constructor uses the ClassName._() syntax. It's accessible only within the same file — nothing outside can call it directly.

class ToyBox {
  ToyBox._(); // private constructor

  static final ToyBox _instance = ToyBox._();

  factory ToyBox() => _instance;
}

Now ToyBox._() can't be called from anywhere outside this file. Only the class itself uses it — which makes it the foundation of the Singleton pattern, utility-only classes, and any situation where you want precise control over instantiation.


Private Constructors for Static-Only Classes

One of the most practical uses of private constructors is preventing instantiation of classes that exist purely as namespaces for static methods or constants.

class MathUtils {
  MathUtils._(); // prevents instantiation

  static const double pi = 3.14159;

  static double areaOfCircle(double radius) => pi * radius * radius;
}

void main() {
  print(MathUtils.areaOfCircle(5)); // works fine
  // var m = MathUtils();           // compile error
}

The private constructor sends a clear signal: this class isn't meant to be used as an object. You use its static members. Creating an instance makes no sense.

You'll see this pattern throughout Flutter's own codebase — Colors, Icons, and similar classes follow the same approach.


Factory Constructors: Smart Object Creation

A factory constructor looks like a regular constructor from the outside, but internally it has full control over what gets returned. It doesn't have to create a new instance every time.

class ToyBox {
  static final ToyBox _instance = ToyBox._();

  ToyBox._();

  factory ToyBox() {
    return _instance; // same instance every time
  }
}

void main() {
  var box1 = ToyBox();
  var box2 = ToyBox();
  print(box1 == box2); // true
}

Factory constructors are useful when you need to return a cached instance, choose between subclasses at runtime, or perform validation before construction. They hide that complexity from the caller.


Factory Constructors That Return Subclasses

A factory constructor can act as a dispatch mechanism — the caller asks for a Shape, and the factory decides which concrete type to return.

abstract class Shape {
  factory Shape(String type) {
    if (type == 'circle') return Circle();
    if (type == 'square') return Square();
    throw ArgumentError('Unknown shape type: $type');
  }
}

class Circle implements Shape {}
class Square implements Shape {}

void main() {
  var s = Shape('circle');
  print(s.runtimeType); // Circle
}

This is encapsulation in practice. The caller doesn't need to know which class they're getting — they just ask for a shape and work with the result.


Private Functions: Keeping Internals Internal

Private functions — any method prefixed with _ — are inaccessible outside the library. They let you keep your class's public surface area small and clean while moving implementation details out of sight.

class PasswordValidator {
  bool isValid(String password) {
    return _hasMinLength(password) && _hasUppercase(password);
  }

  bool _hasMinLength(String password) => password.length >= 8;
  bool _hasUppercase(String password) => password.contains(RegExp(r'[A-Z]'));
}

The public API is just isValid(). The two private helpers do the actual work but are invisible to anyone using this class. This is what good encapsulation looks like — you expose what's necessary and hide everything else.


Real-World Use Cases

Singleton for shared services

When you want a single shared instance across the entire app:

class DatabaseManager {
  static final DatabaseManager _instance = DatabaseManager._();

  DatabaseManager._();

  factory DatabaseManager() => _instance;

  void connect() => print('Connected to DB!');
}

Every call to DatabaseManager() returns the same object. No matter how many places in the app call it, they're all working with the same instance.

Factory for JSON deserialization

This is the most common real-world use of factory constructors in Flutter:

class User {
  final String name;
  final int age;

  User._(this.name, this.age);

  factory User.fromJson(Map<String, dynamic> json) {
    return User._(json['name'], json['age']);
  }
}

The factory hides parsing logic from the caller. If you later need to add validation or caching, you do it inside the factory — none of the calling code changes.

Static-only utility class

class DateUtils {
  DateUtils._();

  static String today() => DateTime.now().toIso8601String();
}

Nothing stops callers from misusing DateUtils as an object — except this private constructor.

Private helpers for internal logic

class EmailValidator {
  bool isValid(String email) => _containsAtSymbol(email) && _isProperLength(email);

  bool _containsAtSymbol(String email) => email.contains('@');
  bool _isProperLength(String email) => email.length >= 6;
}

One clean public method. Two private helpers doing the work behind the scenes.


When to Use Each

ConceptWhen to reach for it
Private constructorWhen you need a Singleton, or a class that should never be instantiated
Factory constructorWhen object creation involves logic, caching, or subclass selection
Private functionsWhen internal helpers shouldn't be part of the public API
Static-only classWhen the class is a container for constants or utility methods

The Core Idea

Private constructors and factory methods aren't clever tricks — they're the tools Dart gives you for writing APIs that are hard to misuse. The more explicitly you define how your classes should be created and accessed, the less time you spend debugging code that used them in ways you didn't anticipate.

Good code hides what should be hidden and exposes only what's meant to be used. These three features — private constructors, factory constructors, and private functions — are how you enforce that in Dart.