The Factory Pattern in Dart — Construction with a Brain
Calling Foo() hard-wires you to one class and gives you no room to think. Dart's factory constructor turns construction into a decision — cache, validate, or pick a subtype — all behind the same call.
The problem — a constructor is a hard wire
Call Circle() and you get a Circle. Always. That's the deal with a normal constructor: you name a concrete class, and you're wired straight to it. No room to cache, no room to validate, no room to hand back something smarter.
Most of the time that's fine. But construction isn't always so simple. Sometimes building an object needs logic.
• You want to reuse an instance instead of allocating a new one every time.
• You want to reject bad inputs before an object even exists.
• You want to decide which concrete type to build based on what you're handed.
A plain constructor can't do any of that. It runs, it fills the fields, it returns a brand new instance of exactly that class. Full stop.
// We want one shared logger per name.
// A normal constructor can't give us that —
// every call builds a fresh object.
class Logger {
final String name;
Logger(this.name);
}
final a = Logger('net');
final b = Logger('net');
print(identical(a, b)); // false — two separate objects
And there's a deeper version of the same problem. The caller has to know the exact class name. Write Circle() everywhere and you've scattered that concrete dependency across the codebase. Want to swap in a cached circle, or pick a shape from a string? You'd have to touch every call site. The constructor leaves you no seam to slip logic into.
What we want is a call that looks like a constructor but is allowed to think first. Dart has exactly that, and it's a keyword.
Dart's factory constructor — a constructor that can think
A normal constructor is generative: it generates a fresh instance and you have no say in that. Mark it factory instead and the rules change. A factory constructor doesn't have to create anything new. It just has to return an instance of the class — and it can get that instance however it likes.
Here's the syntax. Same call site, a body that decides.
class Logger {
final String name;
// The real, private constructor that actually builds.
Logger._internal(this.name);
// The factory: called like Logger('net'), but it
// returns from a cache instead of always allocating.
static final _cache = <String, Logger>{};
factory Logger(String name) {
return _cache.putIfAbsent(name, () => Logger._internal(name));
}
}
final a = Logger('net');
final b = Logger('net');
print(identical(a, b)); // true — same shared object
Two things make a factory constructor different, and both matter.
It returns, it doesn't initialise. A generative constructor has no
return — it fills this and you're done. A factory constructor has a body with an explicit return, and there is no this. It can't touch instance fields, because at the point it runs there's no instance yet. That's the trade: you lose this, you gain a decision.
It can return a subtype. The only constraint is that what you return is the class or something that is-a the class. So a factory on
Shape can hand back a Circle or a Square. That's the seam the plain constructor never gave us — and it's the next topic.
Factory method — pick the concrete type at runtime
Now the payoff. Because a factory can return a subtype, you can put one in front of a family of classes and let it choose which one to build — at runtime, from whatever input you're handed.
Say you decode shapes from a config string. The caller asks for a Shape and shouldn't care which concrete class shows up.
abstract interface class Shape {
double get area;
// One door. The right concrete type walks out.
factory Shape(String kind, double size) {
return switch (kind) {
'circle' => Circle(size),
'square' => Square(size),
_ => throw ArgumentError('Unknown shape: \$kind'),
};
}
}
class Circle implements Shape {
final double r;
Circle(this.r);
@override
double get area => 3.14159 * r * r;
}
class Square implements Shape {
final double side;
Square(this.side);
@override
double get area => side * side;
}
Look at the call site.
Shape('circle', 10) reads exactly like a constructor, yet it hands back a Circle. The caller depends on Shape and nothing else.
final s = Shape('circle', 10);
print(s.area); // 314.159
print(s is Circle); // true — but the caller didn't ask for one
This is the classic "factory method" use: choosing an implementation behind a shared interface. Platform-specific code lives here too — a Storage() factory returning WebStorage on the web and FileStorage on mobile, with every caller blissfully unaware which one it got.
Factories are everywhere in Flutter
Once you know the shape, you'll see factories threaded through the framework and through every app you write. Some are named the pattern; most just quietly use it.
The .of(context) idiom. Theme.of(context), MediaQuery.of(context), Navigator.of(context) — these are factory-method-style lookups. Theme.of is a static method that walks up the widget tree and returns the nearest inherited ThemeData. You ask for theme data; the method finds and hands back the right instance. You never construct it yourself.
// You don't build ThemeData. You ask for it,
// and the framework returns the inherited one.
final theme = Theme.of(context);
final color = theme.colorScheme.primary;
The .fromJson factory. Every model class you parse from an API uses this convention — a factory constructor that takes a decoded Map and returns a typed object.
class User {
final int id;
final String name;
User._(this.id, this.name);
factory User.fromJson(Map<String, dynamic> json) {
return User._(json['id'] as int, json['name'] as String);
}
}
And there are factories hiding in plain sight.
Icon sits in front of a glyph lookup. List.from, Map.fromEntries, Uri.parse — all of these take some input and decide what to build. Construction with a brain, wherever the input needs interpreting.
Factories, const, and caching
Caching is where factory constructors earn their keep. If two callers ask for the same thing, why build it twice? The factory holds a store and hands back what it already made.
class Currency {
final String code;
const Currency._(this.code);
static final _pool = <String, Currency>{};
factory Currency(String code) {
return _pool.putIfAbsent(code, () => Currency._(code));
}
}
final usd1 = Currency('USD');
final usd2 = Currency('USD');
print(identical(usd1, usd2)); // true — one object, shared
Now the rule you have to keep straight. A factory constructor cannot itself be const. A const constructor has to be a compile-time recipe the compiler can evaluate and canonicalise. A factory runs arbitrary code at runtime — a cache lookup, a switch, a throw. The two ideas don't mix, and Dart won't let you write const factory.
But here's the nuance that trips people up: a factory can return an instance that was built by a
const constructor. So you still get canonicalisation — just chosen at runtime rather than guaranteed at compile time.
class Glyph {
final int code;
const Glyph._(this.code); // const, canonicalised
// Not const itself, but it returns canonicalised
// const instances. Two 'star' lookups share one object.
factory Glyph(String name) => switch (name) {
'star' => const Glyph._(0x2605),
'heart' => const Glyph._(0x2665),
_ => throw ArgumentError(name),
};
}
So the boundary is sharp. The factory is runtime logic; the things it returns can still be compile-time constants. You lose const on the door and keep it on what walks through.
Abstract Factory — families of related objects
A single factory picks one object. Sometimes you need to pick a whole family of objects that are meant to work together — and that's the Abstract Factory pattern.
The classic Flutter example is platform styling. On iOS you want a CupertinoApp, a sliding navigation bar, a rounded switch — a matching set. On Android you want the Material equivalents. You don't want to mix one from each; a Cupertino switch inside a Material app looks wrong. So you choose the factory once, and every widget it produces belongs to the same family.
In code, an abstract factory is an interface whose methods each build one member of the family. Concrete factories implement it per platform.
abstract interface class WidgetKit {
Widget button(String label, VoidCallback onTap);
Widget toggle(bool value, ValueChanged<bool> onChanged);
}
class MaterialKit implements WidgetKit {
@override
Widget button(String label, VoidCallback onTap) =>
ElevatedButton(onPressed: onTap, child: Text(label));
@override
Widget toggle(bool v, ValueChanged<bool> onChanged) =>
Switch(value: v, onChanged: onChanged);
}
// CupertinoKit implements the same interface with
// CupertinoButton and CupertinoSwitch.
// Choose the family once.
final WidgetKit kit = Platform.isIOS ? CupertinoKit() : MaterialKit();
kit.button('Save', save); // the right button, guaranteed to match
The whole screen now leans on WidgetKit, never on a concrete widget. Swap the kit and the entire UI re-skins, every part still matching its siblings.
When to use a factory — and when not to
A factory is construction with logic bolted on. That logic has a cost, so reach for it only when a plain constructor genuinely can't do the job.
Use a factory when:
• You need to return a cached or shared instance instead of always allocating.
• You need to choose a concrete subtype at runtime — from a string, a platform, a config.
• Construction needs validation or transformation, like parsing a Map in fromJson.
• You want a named, intention-revealing way to build, such as Colour.fromHex('#9333ea').
Stick with a plain constructor when:
• You just need to set fields. A factory here is pure ceremony.
• You want const. A factory can't be const, so a value type you'd like canonicalised at compile time should keep a const generative constructor.
• Subclasses must call super. A factory isn't part of the initialisation chain — a subclass can't invoke a factory constructor with super, so a base class meant to be extended usually needs a generative constructor.
The trade-offs, plainly. A factory buys you decoupling and a seam for caching or substitution. It costs you this, costs you const, and adds a layer the reader has to step through to see what's really built. The decoupling is real and worth paying for when the construction is doing something. When it isn't, you're wrapping a one-line job in a keyword for no reason.
And notice the through-line back to Strategy. Strategy decouples which behaviour runs; Factory decouples which object gets built. Both push a concrete decision behind an interface so the caller stays ignorant — and an abstract factory is really just a strategy whose job happens to be construction.
Test your understanding
7 questions
Seven questions on the factory pattern — factory constructors, returning subtypes and cached instances, the const boundary, abstract factories, and where factories live inside Flutter.