All about the factory constructor in Dart

And when to use it

All about the factory constructor in Dart
No time to read?
tl;dr

Generally speaking, use a factory in situations where you don’t necessarily want to return a new instance of the class itself and / or want to hide the implementation details of the object creation from the caller.

Use cases:

  • You want to return a pre-existing instance of your class (e. g. cache)
  • You want to return a subclass instance instead of the class itself
  • You have complex initialization of a final variable you don’t want to happen in the initializer list

Having inspected some of the framework classes, third party packages or Dart / Flutter docs, you might have stumbled across the factory keyword and wondered what that means.

In this article, we are going to clarify:

  • The meaning of the keyword
  • When you should use it
  • The difference between factory and a generative constructor
  • The differences between factory and static

Factory pattern

Before we go deeper into the syntax and the semantics of the factory keyword, let’s examine the origins of it.

Because the factory keyword in Dart is actually only syntactic sugar to express something that is language-agnostic. That is the underlying pattern which is called the factory pattern or factory method pattern.

Instead of using a default constructor, a method defined on every class and named the same, the idea is to let the creation happen somewhere else.

A Moment to Think 💡
Basically, the default constructor (the one you call with Cat()) is nothing else but a static method defined on a class (Cat) whose return type must be of the same type (Cat). The main difference compared to a “normal” static function on a class is the inability to change its return type.

The main benefits of using the factory design pattern are the following:

  • The responsibility of creating objects does not lie within the class itself but in a separate class (the factory class) which implements an interface
  • Object creation being bound to the caller class lacks flexibility as you are unable to change the concrete object instantiation independently from the class which implies a coupling. This coupling is not given when using the pattern

In other words: A Breeder does not need to know how to instantiate a Cat because Cats are being produced in factories 🙀🏭. The breeder only says makeACat() and the factory returns a new Cat.

This has the advantage that the Breeder does not change his behavior if the way Cats are produced, changes.

To conclude: It can be used to create objects without exposing the underlying implementation details to the caller.

Constructor types

If this confuses you, don’t worry - we will get to some examples right away. But first, let’s clarify some notions:

In Dart there are generative constructors and factory constructors, which may be respectively named or unnamed.

An example for a generative constructor is the default constructor that is being created automatically for every class.

Let’s consider an exemplary class in order to get a better understanding of the different constructor types:

1class Cat {
2  final Color color;
3}

In our example, there is a Cat class with only one property: color of type Color.

The constructor types would look as follows:

GenerativeFactory
Unnamed
Cat(this.color);
factory Cat(Color color) {
  return Cat(color);
}
Named
Cat.colored(this.color);
factory Cat.colored(Color color) {
  return Cat(color);
}
Tip

Be aware that you are not allowed to create a factory constructor being named like an already existing constructor - either generative or factory and no matter if named or unnamed.

The only exception for this is when you define an unnamed factory constructor without having explicitly defined an unnamed generative constructor. When you don’t define an unnamed constructor, it will be generated for you automatically and when you defined a factory constructor then, it will be overridden.

The keyword in Dart

The factory keyword is not a 1:1 accurate implementation of how you might know the pattern from classic OOP languages like C++ or Java.

That’s because the idea there is to have a separate class that handles the object creation (like a CatFactory 😸).

By using a factory constructor, however, you still have the logic of the object creation inside the same class. With the exception of instantiating subclasses, which is also possible with factory constructors.

When you should use it

"Use the factory keyword when implementing a constructor that doesn’t always create a new instance of its class. For example, a factory constructor might return an instance from a cache, or it might return an instance of a subtype. Another use case for factory constructors is initializing a final variable using logic that can’t be handled in the initializer list."

The docs basically mention three use cases:

  • Returning an instance from a cache
  • Returning an instance of a subtype
  • Initializing a final variable

Let’s explain the docs’ example one by one.

Instance from a cache

Let’s pretend we have a ColorComputer that takes very long to compute the color of the cat. We also have a CatCache that stores the last created colored cat to prevent having to execute the heavyColorComputation() everytime a Cat.colored is instantiated.

 1class Cat {
 2  Cat(this.color);
 3
 4  factory Cat.colored(CatCache catCache) {
 5    Cat? cachedCat = catCache.getCachedColorCat();
 6
 7    if (cachedCat != null) {
 8      return cachedCat;
 9    }
10
11    Color color = ColorComputer().heavyColorComputation();
12    return Cat(color);
13  }
14
15  final Color color;
16}

As you can see, we can use the factory constructor for this use case because we might return an existing instance of Cat (if the cache hits).

Complex final variable initialization

If you have a more complicated initialization of a final variable that can’t really be handled inside the initializer list, you can use a factory constructor instead.

 1class Cat {
 2  Cat._({
 3    required this.id,
 4    required this.name,
 5    required this.age,
 6    required this.color
 7  });
 8
 9  final int id;
10  final String name;
11  final int age;
12  final Color color;
13
14  factory Cat.fromJson(Map<String, dynamic> json) {
15    DateTime now = DateTime.now();
16    late Color color;
17
18    if (now.hour < 12) {
19      color = const Color(0xFF000000);
20    }
21    else {
22      color = const Color(0xFFFFFFFF);
23    }
24
25    return Cat._(
26      id: json['id'],
27      name: json['name'],
28      age: json['age'],
29      color: color,
30    );
31  }
32
33  void meow() {
34    print('Meow!');
35  }
36
37  void whoAmI() {
38    print('I am $name ($id) and I am $age years old. My color is $color.');
39  }
40}

Here, the initialization of the color variable requires a bit of logic. Because there are multiple statements to be made, this better be handled inside a factory constructor.

Let’s call the fromJson constructor and inspect its output:

1const String myJson = '{"id": 5, "name": "Herbert", "age": 7}';
2final Cat decodedCat = Cat.fromJson(jsonDecode(myJson));
3
4decodedCat.meow();
5decodedCat.whoAmI();

This results in the following output:

"Meow!
I am Herbert (5) and I am 7 years old. My color is Color(0xffffffff)."

— decodedCat

Instance of a subtype

Another use case for a factory constructor is to return an instance of a derived class. This is not possible with the generative constructor.

It can be useful if the logic for deciding, which subclass to return, is always the same throughout your application. Instead of duplicating it, you might consider encapsulating it in a central place.

 1abstract class Cat {
 2  Cat({required this.age});
 3
 4  int age;
 5
 6  factory Cat.makeCat(bool aggressive, int age) {
 7    if (aggressive || age < 3) {
 8      return AggressiveCat(age: age);
 9    }
10
11    return DefensiveCat(age: age);
12  }
13
14  void fight();
15}
16
17class AggressiveCat extends Cat {
18  AggressiveCat({required super.age});
19
20  @override
21  void fight() {
22    print('Where dem enemies at?!');
23  }
24}
25
26class DefensiveCat extends Cat {
27  DefensiveCat({required super.age});
28
29  @override
30  void fight() {
31    print('Nah, I\'m staying!');
32  }
33}

Difference between a generative constructor and a factory constructor

What we have learned: a generative constructor always returns a new instance of the class, which is why it doesn’t need the return keyword.

A factory constructor on the other hand is bound to a lot looser constraints. For a factory constructor, it suffices if the class it returns is the same type as the class or it satisfies its interface (e. g. a subclass). This could be a new instance of the class, but could also be an existing instance of the class (as seen in the cache example above).

A factory can use control flow to determine what object to return, and must therefore utilize the return keyword. In order for a factory to return a new class instance, it must first call a generative constructor.

All of the minor and major differences are listed in the following breakdown:

  • Factory constructors can invoke another constructor (and needs to if it doesn’t return an existing instance)
  • Factory constructors are unable to use an initializer list (because they do not directly create a new instance)
  • Factory constructors as opposed to a generative constructor are permitted to return an existing object
  • Factory constructors are permitted to return a subclass
  • Factory constructors don’t need to initialize instance variables of the class
  • A derived class cannot invoke a factory constructor of the superclass. As a consequence, a class providing solely factory constructors can’t be extended.
    • The compiler will complain otherwise: “The generative constructor is expected, but a factory was found”
  • Generative constructors can not set final properties in the constructor body
  • Generative constructors can be const and don’t need to be redirecting

Difference between static and factory

You might ask yourself: “But why do I need this keyword? Can’t I just use usual static methods?!”.

In fact, there is not much difference between a static method and a factory constructor. Although, the syntax differs a bit.

Generally speaking, a static method has looser constraints but also less syntactic sugar. That’s because every factory constructor is (technically) a static method, but not every static method is a factory constructor. So if you define a factory constructor, the compiler knows your intention and can support you. The biggest difference is probably that the return type of a factory constructor is set to the current class or derived classes while for a static method you can provide any return type.

If we use one of the above examples, we can see that we can achieve the same result with a static method:

 1class Cat {
 2  Cat._({
 3    required this.id, 
 4    required this.name, 
 5    required this.age, 
 6    required this.color
 7  });
 8
 9  final int id;
10  final String name;
11  final int age;
12  final Color color;
13  
14  static Cat catfromJson(Map<String, dynamic> json) {
15    DateTime now = DateTime.now();
16    late Color color;
17    
18    if (now.hour < 12) {
19      color = const Color(0xFF000000);
20    }
21    else {
22      color = const Color(0xFFFFFFFF);
23    }
24  
25    return Cat._(
26      id: json['id'],
27      name: json['name'],
28      age: json['age'],
29      color: color,
30    );
31  }
32
33  factory Cat.fromJson(Map<String, dynamic> json) {
34    DateTime now = DateTime.now();
35    late Color color;
36    
37    if (now.hour < 12) {
38      color = const Color(0xFF000000);
39    }
40    else {
41      color = const Color(0xFFFFFFFF);
42    }
43  
44    return Cat._(
45      id: json['id'],
46      name: json['name'],
47      age: json['age'],
48      color: color,
49    );
50  }
51  
52  void meow() {
53    print('Meow');
54  }
55  
56  void whoAmI() {
57    print('I am $name ($id) and I am $age years old. My color is $color.');
58  }
59}

In terms of code readability it is still a good practice to use a factory constructor instead of static methods when creating instances. This makes the purpose (object creation) more obvious.

To give you a complete overview, I have listed all of the differences in the following breakdown:

  • A factory constructor as opposed to a static method can only return instance of the current class or subclasses
  • A static method can be async. Since factory constructors need to return an instance of the current or subclass, it is unable to return a Future
  • A static method can not be unnamed whereas factory constructors can
  • If you specify a named factory constructor, the default constructor is automatically removed
  • Factory constructors can use a special syntax for redirecting
  • It’s not mandatory for a factory constructor to specify generic parameters
  • Factory constructors can be declared const
  • A factory constructor can not return a nullable type.
  • When generating dartdoc documentation, the factory constructors will be listed under “Constructors”. static method will be found elsewhere at the bottom of the documentation

So the devil is in the details but from a low-level perspective, it doesn’t matter if you use a static method or factory constructor.

Conclusion

The factory keyword can be helpful if the instance creation of a class exceeds a certain complexity. This can be the case for caching or when using otherwise complex logic.

Apart from that, nothing goes against using a static method. Although factory constructors are exactly meant for instance creation whereas static method have a lot broader field of use.

Ultimately, it comes down to personal preference.

Comment this 🤌

You are replying to 's commentRemove reference