Understanding Factory Constructors in Dart/Flutter: An Overview
Written on
Introduction
Factory constructors may not be essential for developing a Flutter application, but they offer valuable functionality for various scenarios. These constructors can facilitate the singleton design pattern, aid in deserialization, help instantiate subclasses, or simply enhance code clarity. While factory constructors and static methods often appear similar, understanding their distinctions is key. This topic is aimed at those familiar with Dart and Flutter who wish to deepen their comprehension.
When to Use Factory Constructors?
Here, we will explore several scenarios where factory constructors are particularly beneficial, accompanied by code examples.
Singleton Pattern
Implementing the singleton pattern ensures that a class has only one instance. This is useful for complex classes that require significant computation or need to restrict access, such as a database connection. Below is an example demonstrating this concept, which can be tested on DartPad.
In this example, the database instance is created once (1). The constructor is private (2), ensuring that instantiation can only occur through the factory method, which always returns the same instance (3).
class Database {
static final Database _database = Database._internal(); // (1)
factory Database() => _database; // (3)
Database._internal(); // (2)
}
// Create the singleton
void main() {
Database db = Database();
Database db2 = Database();
print(identical(db, db2)); // true
}
Deserialization
Most applications interact with APIs, and when data is received as a map, you often want to create corresponding classes for easier manipulation. Factory constructors allow you to accept a map as input and produce an object. Check out this example of user deserialization on DartPad.
Here, user data is extracted from a map (1), which is a common practice. When the factory is invoked (2), it assigns specific keys to attributes of a user object (3).
class User {
final String name;
final int age;
User({required this.name, required this.age});
factory User.fromJson(Map<String, dynamic> json) => User(
name: json['name'],
age: json['age'],
); // (3)
}
// Use deserialization
void main() {
final karlMap = {
'name': 'Karl',
'age': 32,
}; // (1)
final karl = User.fromJson(karlMap); // (2)
print(karl.name); // Karl
print(karl.age); // 32
}
Instantiating Subclasses
Factory constructors enable the instantiation of different subclasses from a parent class, which can be advantageous when creating instances based on random factors or time.
Imagine a game where players first encounter easier enemies that become tougher over time. A ghost (1) represents a low-level foe, while a dragon (2) signifies a more formidable opponent. As players progress, they encounter more dragons (3,4). Although a static method could achieve this, using a factory constructor can yield a more elegant solution.
class Enemy {
final int strength = 0;
final String name = '';
factory Enemy.getEnemy({required int timeInSeconds}) {
if (timeInSeconds < 1000) {
return Ghost();} else {
return Dragon();}
}
}
class Ghost implements Enemy { // (1)
@override
final int strength = 10;
@override
final String name = "Ghost";
}
class Dragon implements Enemy { // (2)
@override
final int strength = 100;
@override
final String name = "Dragon";
}
void main() {
Enemy enemy = Enemy.getEnemy(timeInSeconds: 1); // (3)
print(enemy.strength); // prints 10
print(enemy.name); // prints Ghost
Enemy enemy2 = Enemy.getEnemy(timeInSeconds: 1000); // (4)
print(enemy2.strength); // prints 100
print(enemy2.name); // prints Dragon
}
Syntax Sugar
In some cases, opting for a factory constructor over a static method is preferable due to simpler syntax. These distinctions will be further discussed in the section on the difference between factory constructors and static methods.
What is the Difference Between a Factory Constructor and a Static Method?
While factory constructors can often be substituted with static methods, their unique features make them indispensable in certain scenarios. Renowned Flutter developer Remi Rousselet, known for creating popular packages like provider, riverpod, and freezed, provides insights into this distinction in his article. Here are some of his key points:
- Factory constructors may be unnamed.
- They do not require specifying generic parameters.
- Naming a factory constructor eliminates the default constructor.
- Factory constructors allow shorthand syntax for redirecting to other constructors.
- They can be declared as const.
- Factory constructors cannot be async.
For more details and examples, refer to Rousselet's original blog post.
Conclusion
While factory constructors can often be replaced with static methods, they offer specific advantages in terms of code readability and ease of use, particularly for singletons and subclass instantiation. If you have questions or additional insights, feel free to share your thoughts in the comments. I welcome any suggestions for future article topics as well.
If you enjoyed this article, please engage by clapping, highlighting, commenting, and sharing. Technical articles, in particular, have been affected by the recent changes to Medium's Partner Program, so your interaction is greatly appreciated!
Resources
- Factory constructors (documentation, Dart)
- Factory Constructors in Dart (YouTube, Programming Point)
- Understanding Factory constructor code example — Dart (StackOverFlow, Ananta K Roy)
- FACTORY CONSTRUCTOR IN DART (blog, dart-tutorial.com)
- THE DIFFERENCE BETWEEN A “FACTORY CONSTRUCTOR” AND A “STATIC METHOD” (blog, dashoverflow [Remi Rousselet])
- How do you build a Singleton in Dart? (StackOverFlow, Seth Ladd)
More Articles
- What is the difference between MVC and MVVM (for Flutter)?
- Flutter: Why you will regret using GetX (2023)
- 16 ideas to improve your Flutter skills (advanced roadmap)