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 _instancestores 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,brepresent red, green, and blue components.finalmeans once anColorobject is created, these values cannot change.
Second, create a cache to reuse identical colors:
static final Map<int, Color> _cache = {};Code language: Dart (dart)staticmeans this map belongs to the class, not to individual objects. So there is one shared cache for allColorobjects._cacheuses aintkey toColorvalue.- The leading underscore
_cachemakes 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.