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.
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.
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.
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.
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.
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
List all Dart tutorials.