ZetCode

Factory Constructors in Dart

last modified June 8, 2025

This tutorial explores factory constructors in Dart, a special kind of constructor that provides flexible object creation mechanisms. Unlike generative constructors, factory constructors can return instances from cache, subclasses, or even completely different types.

Factory Constructor Overview

Factory constructors are declared using the factory keyword and differ from regular constructors in several key ways. They don't create new instances directly but instead control the instantiation process.

Key characteristics of factory constructors:

Feature Factory Constructor Generative Constructor
Instantiation Controls object creation Always creates new instance
Return Can return existing/cached instances Always returns new instance
Subclasses Can return subclass instances Only returns current class
Initializers No access to this Can use initializer list

Factory constructors are commonly used to implement design patterns like Singleton, Factory Method, or Object Pool. They're also useful for deserialization and when working with immutable classes that need flexible construction.

Basic Factory Constructor

This example demonstrates a simple factory constructor that implements a cache for previously created instances. The Logger class ensures only one instance exists per tag.

basic_factory.dart
class Logger {
  final String tag;
  static final Map<String, Logger> _cache = {};

  // Private generative constructor
  Logger._internal(this.tag);

  // Factory constructor
  factory Logger(String tag) {
    return _cache.putIfAbsent(tag, () => Logger._internal(tag));
  }

  void log(String message) {
    print('[$tag] $message');
  }
}

void main() {
  var logger1 = Logger('main');
  var logger2 = Logger('main');
  var logger3 = Logger('network');

  logger1.log('Hello There');
  logger2.log('Hello Again');

  print('logger1 and logger2 are the same: ${identical(logger1, logger2)}');
  print('logger1 and logger3 are the same: ${identical(logger1, logger3)}');
}

The Logger class uses a private generative constructor (_internal) and a public factory constructor. The factory maintains a cache of instances and returns existing ones when requested with the same tag.

The putIfAbsent method ensures thread-safe creation of new instances only when needed. This pattern is useful for managing expensive resources or maintaining single instances per unique identifier.

$ dart run basic_factory.dart
[main] Hello There
[main] Hello Again
logger1 and logger2 are the same: true
logger1 and logger3 are the same: false

Factory with Subclass Instantiation

Factory constructors can return instances of subclasses. This example shows a Shape factory that creates different shape types based on input parameters.

subclass_factory.dart
abstract class Shape {
  double get area;
  
  factory Shape(String type, double size) {
    switch (type.toLowerCase()) {
      case 'circle':
        return Circle(size);
      case 'square':
        return Square(size);
      default:
        throw ArgumentError('Unknown shape type: $type');
    }
  }
}

class Circle implements Shape {
  final double radius;
  
  Circle(this.radius);
  
  @override
  double get area => 3.14159 * radius * radius;
  
  @override
  String toString() => 'Circle(radius: $radius)';
}

class Square implements Shape {
  final double side;
  
  Square(this.side);
  
  @override
  double get area => side * side;
  
  @override
  String toString() => 'Square(side: $side)';
}

void main() {
  var circle = Shape('circle', 5.0);
  var square = Shape('square', 4.0);
  
  print(circle);
  print('Area: ${circle.area}');
  
  print(square);
  print('Area: ${square.area}');
  
  try {
    var triangle = Shape('triangle', 3.0);
    print(triangle.area);
  } catch (e) {
    print('Error: $e');
  }
}

The Shape abstract class defines a factory constructor that acts as a simple factory method. Based on the input type, it creates and returns either a Circle or Square instance.

This pattern hides the concrete implementations from clients, who only work with the abstract Shape interface. The factory handles the instantiation logic and validates input parameters.

$ dart run subclass_factory.dart
Circle(radius: 5.0)
Area: 78.53975
Square(side: 4.0)
Area: 16.0
Error: ArgumentError: Unknown shape type: triangle

JSON Deserialization Factory

Factory constructors are commonly used for JSON deserialization. This example shows a User class with a factory that creates instances from JSON maps.

json_factory.dart
class User {
  final String id;
  final String name;
  final int age;
  final DateTime signUpDate;

  // Private generative constructor
  User._({
    required this.id,
    required this.name,
    required this.age,
    required this.signUpDate,
  });

  // Factory for JSON deserialization
  factory User.fromJson(Map<String, dynamic> json) {
    if (!json.containsKey('id') ||
        !json.containsKey('name') ||
        !json.containsKey('age') ||
        !json.containsKey('signUpDate')) {
      throw ArgumentError('Missing required fields in JSON');
    }

    return User._(
      id: json['id'] as String,
      name: json['name'] as String,
      age: json['age'] as int,
      signUpDate: DateTime.parse(json['signUpDate'] as String),
    );
  }

  @override
  String toString() => 'User($id, $name, $age, ${signUpDate.toLocal()})';

}

void main() {
  var json = {
    'id': 'u123',
    'name': 'Alice',
    'age': 30,
    'signUpDate': '2023-05-25T10:30:00Z',
  };

  var user = User.fromJson(json);
  print(user);

  // Handle potential invalid JSON
  try {
    var badJson = {'id': 'u124', 'name': 'Bob'};
    var invalidUser = User.fromJson(badJson);
    print(invalidUser.name);
  } catch (e) {
    print('Error creating user: $e');
  }
}

The User.fromJson factory validates and converts JSON data into a properly typed User object. It uses a private constructor to ensure all fields are properly initialized.

This pattern is particularly useful when working with APIs or persistent storage. The factory can handle data validation, type conversion, and provide meaningful error messages for invalid input.

$ dart run json_factory.dart
User(id: u123, name: Alice, age: 30, signed up: 2023-05-25 10:30:00.000)
Error creating user: type 'Null' is not a subtype of type 'int' in type cast

Singleton Pattern with Factory

Factory constructors can implement the Singleton pattern, ensuring a class has only one instance. This example shows a thread-safe Singleton in Dart.

singleton_factory.dart
class AppConfig {
  final String environment;
  final String apiUrl;

  // Private static instance
  static AppConfig? _instance;

  // Private generative constructor
  AppConfig._({
    required this.environment,
    required this.apiUrl,
  });

  // Factory constructor for Singleton access
  factory AppConfig.getInstance() {
    return _instance ??= AppConfig._(
      environment: 'production',
      apiUrl: 'https://api.example.com',
    );
  }

  // Factory with lazy initialization
  factory AppConfig({String? environment, String? apiUrl}) {
    _instance ??= AppConfig._(
      environment: environment ?? 'development',
      apiUrl: apiUrl ?? 'https://dev.api.example.com',
    );
    return _instance!;
  }

  void printConfig() {
    print('Environment: $environment');
    print('API URL: $apiUrl');
  }
}

void main() {
  var config1 = AppConfig.getInstance();
  var config2 = AppConfig.getInstance();

  print('config1 and config2 are the same: ${identical(config1, config2)}');
  config1.printConfig();

  // Alternative initialization
  var devConfig = AppConfig(environment: 'staging');
  print('\nDev config:');
  devConfig.printConfig();
}

The AppConfig class demonstrates two approaches to Singleton implementation. The getInstance factory provides strict Singleton access, while the named factory allows one-time configuration.

The null-aware assignment operator (??=) ensures thread-safe lazy initialization. This pattern is useful for global configuration or shared resources where multiple instances could cause problems.

$ dart run singleton_factory.dart
config1 and config2 are the same: true
Environment: production
API URL: https://api.example.com

Dev config:
Environment: staging
API URL: https://dev.api.example.com

Complex Object Construction

Factory constructors can encapsulate complex creation logic. This example shows a Document factory that creates different document types based on file content analysis.

complex_factory.dart
abstract class Document {
  final String path;
  final String content;
  
  Document(this.path, this.content);
  
  factory Document.load(String path) {
    // Simulate file reading
    var content = _readFileContent(path);
    
    // Determine document type
    if (path.endsWith('.md') || content.startsWith('#')) {
      return MarkdownDocument(path, content);
    } else if (path.endsWith('.html') || content.contains('<html>')) {
      return HtmlDocument(path, content);
    } else {
      return PlainTextDocument(path, content);
    }
  }
  
  static String _readFileContent(String path) {
    // Simulated file content based on path
    if (path.endsWith('.md')) {
      return '# Markdown Header\n\nContent here';
    } else if (path.endsWith('.html')) {
      return '<html><body>HTML Content</body></html>';
    } else {
      return 'Plain text content';
    }
  }
  
  void display();
}

class MarkdownDocument extends Document {
  MarkdownDocument(super.path, super.content);
  
  @override
  void display() {
    print('Rendering Markdown: ${content.substring(0, 15)}...');
  }
}

class HtmlDocument extends Document {
  HtmlDocument(super.path, super.content);
  
  @override
  void display() {
    print('Rendering HTML: ${content.substring(0, 15)}...');
  }
}

class PlainTextDocument extends Document {
  PlainTextDocument(super.path, super.content);
  
  @override
  void display() {
    print('Displaying text: ${content.substring(0, 15)}...');
  }
}

void main() {
  var documents = [
    Document.load('document.md'),
    Document.load('page.html'),
    Document.load('notes.txt'),
  ];
  
  for (var doc in documents) {
    doc.display();
  }
}

The Document.load factory analyzes both file extension and content to determine the appropriate document type. This encapsulates complex creation logic while providing a simple interface to clients.

The factory handles all the decision-making about which concrete class to instantiate, allowing new document types to be added without changing client code. This is an example of the Factory Method design pattern.

$ dart run complex_factory.dart
Rendering Markdown: # Markdown Head...
Rendering HTML: <html><body>HTM...
Displaying text: Plain text con...

Source

Dart Factory Constructors
Dart Language Tour: Factories

Factory constructors are a powerful feature in Dart that enable flexible object creation patterns. They can manage object caching, implement design patterns, handle deserialization, and encapsulate complex instantiation logic. By using factory constructors appropriately, you can create more maintainable and flexible Dart code.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.