Dart Private Constructors

Summary: In this tutorial, you’ll learn about Dart private constructors.

Introduction to Data private constructors #

In Dart, identifiers starting with _ are private to the library. Based on this privacy rule, you can define a private constructor by using a _ (underscore).

Private constructors can be accessible only within the library. Private constructors are very useful when you want to control the creation of objects.

Here’s a simple example of a private constructor:

class User {
  final String name;
  User._(this.name);
  String toString() => name;
}Code language: Dart (dart)

In this example, User._() is a private constructor. It means that you can call it from the same file (or library):

class User {
  final String name;
  User._(this.name);
  String toString() => name;
}

void main() {
   final user = User._('admin');
   print(user);
}Code language: Dart (dart)

Output:

adminCode language: Dart (dart)

If you attempt to call it from another file, you’ll encounter an error.

Control object creation using factory constructors #

In practice, you often use private constructors with factory constructors to control the object creation. For example:

class Email {
  final String value;

  // private constructor
  Email._(this.value);

  factory Email(String input) {
    // validate email address
    final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
    
    // throw an exception if it is not valid
    if (!emailRegex.hasMatch(input)) {
      throw Exception("Invalid email $input");
    }
    
    // return the email object
    return Email._(input);
  }
}
Code language: Dart (dart)

In this example:

  • The factory constructor performs a validation.
  • Private constructor, _ does the actual assignment.

Note that if you validate in a regular constructor, you cannot prevent other constructors from bypassing validation unless you implement validation in all constructors. For example:

class Email {
  final String value;

  Email(String input): value = input {
    // validate email address
    final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
    
    // throw an exception if it is not valid
    if (!emailRegex.hasMatch(input)) {
      throw Exception("Invalid email $input");
    }
    
    // return the email object
    return Email._(input);
  }
}
Code language: Dart (dart)

Singleton pattern #

Singleton pattern allows a class to have one instance. For example:

class Logger {
  static final Logger _instance = Logger._();
  Logger._();
  factory Logger() => _instance;
}Code language: Dart (dart)

In this example:

  • The private constructor (_) prevents outside code from creating new instances.
  • static final _instance stores the single instance.
  • factory Logger() always returns the same instance.

Note that you can make the code shorter:

class Logger {
  Logger._();
  static final Logger instance = Logger._();
}Code language: Dart (dart)

_ vs Named constructors #

You may see _ and _ with names, for example:

User._()
User._internal()
User._fromJson()
User._cache()Code language: Dart (dart)

They are all private constructors.

Named internal constructors #

Sometimes, you may want multiple constructors but hide internal ones. For example:

class Order {
  final double price;

  Order._internal(this.price);

  factory Order.free() {
    return Order._internal(0);
  }

  factory Order.paid(double price) {
    return Order._internal(price);
  }
}
Code language: Dart (dart)

In this example, we define two factory constructors (free and paid) that create instances via a single internal constructor. There _internal is also a private constructor.

Caching #

The following example shows how to implement caching using a private constructor and a factory constructor:

class Color {
  final int r, g, b;

  static final Map<int, Color> _cache = {};

  Color._(this.r, this.g, this.b);

  factory Color.rgb(int r, int g, int b) {
    if (r < 0 || r > 255) throw ArgumentError('r out of range');
    if (g < 0 || g > 255) throw ArgumentError('g out of range');
    if (b < 0 || b > 255) throw ArgumentError('b out of range');

    final key = (r << 16) | (g << 8) | b;

    // Return existing instance if it was created before
    return _cache.putIfAbsent(key, () => Color._(r, g, b));
  }

  @override
  String toString() => 'Color($r,$g,$b)';
}Code language: Dart (dart)

How it works:

First, store color components as immutable fields:

final int r, g, b;Code language: Dart (dart)
  • r, g, b represent red, green, and blue components.
  • final means once an Color object is created, these values cannot change.

Second, create a cache to reuse identical colors:

static final Map<int, Color> _cache = {};Code language: Dart (dart)
  • static means this map belongs to the class, not to individual objects. So there is one shared cache for all Color objects.
  • _cache uses a int key to Color value.
  • The leading underscore _cache makes it library-private, so only code in the same library (usually file) can access it.

If you request the same (r, g, b) again, the code can return the already-created instance instead of creating a new one.

Third, define a private constructor:

Color._(this.r, this.g, this.b);Code language: Dart (dart)

Fourth, create an object via a factory constructor:

factory Color.rgb(int r, int g, int b) { ... }Code language: Dart (dart)

The factory constructor validates the inputs and returns a cached instance if it exists or a new one otherwise.

Summary #

  • Use private constructors to restrict access, enforce rules, and enable factories and singletons.
Was this tutorial helpful ?