Factory Constructors
Understand how to use factory constructors for flexible object creation in Dart.
What is it?
Factory constructors are special constructors that don't always create a new instance of the class. Instead, they can return existing instances, return subtypes, or perform complex initialization logic before creating an object.
Why does it exist?
Factory constructors exist to:
- Return existing instances (caching)
- Create instances from different data sources
- Return subtypes of the class
- Perform complex initialization
- Implement singleton pattern
- Control object creation
Basic Factory Constructors
Simple Factory Constructor
class User {
final String name;
final int age;
// Private constructor
User._internal(this.name, this.age);
// Factory constructor
factory User(String name, int age) {
// Validation logic
if (name.isEmpty) {
throw ArgumentError('Name cannot be empty');
}
if (age < 0 || age > 150) {
throw ArgumentError('Invalid age');
}
// Create and return instance
return User._internal(name, age);
}
// Factory with default values
factory User.guest() {
return User._internal('Guest', 0);
}
}
// Usage
var user1 = User('Alice', 25);
var user2 = User.guest();
print(user1.name); // Alice
print(user2.name); // Guest
Factory with Caching
class Logger {
final String name;
// Cache of instances
static final Map<String, Logger> _cache = {};
// Private constructor
Logger._internal(this.name);
// Factory constructor with caching
factory Logger(String name) {
return _cache.putIfAbsent(name, () => Logger._internal(name));
}
void log(String message) {
print('[$name] $message');
}
}
// Usage
var logger1 = Logger('App');
var logger2 = Logger('App');
var logger3 = Logger('Network');
print(identical(logger1, logger2)); // true (cached)
print(identical(logger1, logger3)); // false (different)
logger1.log('Starting app'); // [App] Starting app
logger3.log('Connecting...'); // [Network] Connecting...
Factory with Different Types
Returning Subtypes
abstract class Shape {
double get area;
double get perimeter;
}
class Circle extends Shape {
final double radius;
Circle(this.radius);
@override
double get area => math.pi * radius * radius;
@override
double get perimeter => 2 * math.pi * radius;
}
class Rectangle extends Shape {
final double width;
final double height;
Rectangle(this.width, this.height);
@override
double get area => width * height;
@override
double get perimeter => 2 * (width + height);
}
class Square extends Rectangle {
Square(double side) : super(side, side);
}
class ShapeFactory {
// Factory constructor
factory ShapeFactory.create(String type, Map<String, double> params) {
switch (type.toLowerCase()) {
case 'circle':
return Circle(params['radius']!);
case 'rectangle':
return Rectangle(params['width']!, params['height']!);
case 'square':
return Square(params['side']!);
default:
throw ArgumentError('Unknown shape type: $type');
}
}
}
// Usage
var circle = ShapeFactory.create('circle', {'radius': 5});
var rectangle = ShapeFactory.create('rectangle', {'width': 10, 'height': 20});
var square = ShapeFactory.create('square', {'side': 15});
print(circle.area); // 78.54
print(rectangle.area); // 200
print(square.area); // 225
Factory with JSON
Creating from JSON
class Product {
final String id;
final String name;
final double price;
final String category;
final bool inStock;
// Private constructor
Product._internal({
required this.id,
required this.name,
required this.price,
required this.category,
this.inStock = true,
});
// Factory from JSON
factory Product.fromJson(Map<String, dynamic> json) {
return Product._internal(
id: json['id'] as String,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
category: json['category'] as String,
inStock: json['inStock'] as bool? ?? true,
);
}
// Factory with default
factory Product.basic(String name, double price) {
return Product._internal(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: name,
price: price,
category: 'General',
inStock: true,
);
}
// To JSON
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'price': price,
'category': category,
'inStock': inStock,
};
}
// Usage
var product1 = Product.fromJson({
'id': 'P001',
'name': 'Laptop',
'price': 999.99,
'category': 'Electronics',
'inStock': true,
});
var product2 = Product.basic('Mouse', 29.99);
print(product1.name); // Laptop
print(product2.price); // 29.99
Factory Singleton Pattern
Singleton with Factory
class DatabaseConnection {
static DatabaseConnection? _instance;
// Private constructor
DatabaseConnection._internal() {
print('Creating new database connection');
}
// Factory constructor
factory DatabaseConnection() {
_instance ??= DatabaseConnection._internal();
return _instance!;
}
// Instance methods
void query(String sql) {
print('Executing: $sql');
}
void disconnect() {
print('Disconnecting');
_instance = null;
}
}
// Usage
var db1 = DatabaseConnection(); // Creating new database connection
var db2 = DatabaseConnection(); // Returns existing instance
print(identical(db1, db2)); // true
db1.query('SELECT * FROM users');
db2.query('SELECT * FROM products');
db1.disconnect(); // Disconnecting
var db3 = DatabaseConnection(); // Creating new database connection
print(identical(db1, db3)); // false
Factory with Complex Validation
Advanced Factory Constructors
class Email {
final String value;
// Private constructor
Email._(this.value);
// Factory with validation
factory Email(String value) {
if (!_isValid(value)) {
throw FormatException('Invalid email: $value');
}
return Email._(value);
}
// Factory with specific domains
factory Email.company(String value, String domain) {
if (!_isValid(value)) {
throw FormatException('Invalid email: $value');
}
if (!value.endsWith('@$domain')) {
throw FormatException('Email must be from domain: $domain');
}
return Email._(value);
}
// Factory for test emails
factory Email.test() {
return Email._('test@example.com');
}
// Validation helper
static bool _isValid(String email) {
return email.contains('@') && email.contains('.');
}
String get domain => value.split('@')[1];
String get username => value.split('@')[0];
@override
String toString() => value;
}
// Usage
var email1 = Email('user@example.com');
var email2 = Email.company('alice@company.com', 'company.com');
var email3 = Email.test();
print(email1); // user@example.com
print(email2); // alice@company.com
print(email3); // test@example.com
// Throws errors
// var invalid = Email('invalid'); // Error: Invalid email
// var wrongDomain = Email.company('user@gmail.com', 'company.com'); // Error
Factory with Dependency Injection
Flexible Object Creation
abstract class Service {
void execute();
}
class RealService implements Service {
@override
void execute() {
print('Real service executing...');
}
}
class MockService implements Service {
@override
void execute() {
print('Mock service executing...');
}
}
class ServiceProvider {
static bool _useMock = false;
// Set mode
static void useMock(bool use) {
_useMock = use;
}
// Factory constructor
factory ServiceProvider.create() {
if (_useMock) {
return MockService() as ServiceProvider;
} else {
return RealService() as ServiceProvider;
}
}
}
// Usage
ServiceProvider.useMock(true);
var mock = ServiceProvider.create();
mock.execute(); // Mock service executing...
ServiceProvider.useMock(false);
var real = ServiceProvider.create();
real.execute(); // Real service executing...
Best Practices
Use Factory for Complex Creation
// Good: Factory for complex creation
class Configuration {
final Map<String, dynamic> _config;
Configuration._(this._config);
factory Configuration.fromFile(String path) {
var content = File(path).readAsStringSync();
var map = jsonDecode(content);
return Configuration._(map);
}
factory Configuration.fromEnv() {
var map = {
'host': Platform.environment['HOST'] ?? 'localhost',
'port': int.parse(Platform.environment['PORT'] ?? '8080'),
};
return Configuration._(map);
}
factory Configuration.defaults() {
return Configuration._({'host': 'localhost', 'port': 8080});
}
}
Cache Expensive Objects
// Good: Caching expensive objects
class ImageLoader {
static final Map<String, Image> _cache = {};
factory ImageLoader.load(String url) {
return _cache.putIfAbsent(
url,
() => ImageLoader._loadFromNetwork(url),
);
}
ImageLoader._loadFromNetwork(String url) {
// Network request
print('Loading image from: $url');
// Return image
}
}
Common Mistakes
Using Factory for Simple Cases
Wrong:
class User {
final String name;
// Unnecessary factory
factory User(String name) => User._(name);
User._(this.name);
}
Correct:
class User {
final String name;
// Simple constructor
User(this.name);
}
Returning Null
Wrong:
factory User.findById(String id) {
var user = _findUser(id);
if (user == null) {
return null; // Factory shouldn't return null
}
return User._(user);
}
Correct:
factory User.findById(String id) {
var user = _findUser(id);
if (user == null) {
throw ArgumentError('User not found: $id');
}
return User._(user);
}
// Or return a default
factory User.findById(String id) {
var user = _findUser(id);
return user != null ? User._(user) : User._default();
}
Summary
Factory constructors provide flexible object creation patterns. They enable caching, subtype returning, complex validation, and singleton implementation while maintaining clean APIs.
Next Steps
Now that you understand factory constructors, continue to:
Did You Know?
- Factory constructors don't use
this - They can return subtypes
- Factory constructors can be named
- They're useful for dependency injection
- Factory constructors can implement caching
- They work with private constructors
- Factory constructors can be used for validation