Dart (3.13) Module 18 – Professional Dart: code generation, idioms and Flutter
Idiomatic Dart: Effective Dart and patterns
Write Dart the way its own style guide asks, and implement common design patterns with the language features that often make a pattern unnecessary.
What you will learn
- Apply the Effective Dart style, documentation, usage and design rules
- Implement the Strategy, State and Repository patterns the Dart way
- Choose a language feature over a pattern when it removes code
- Recognise the overused constructs that Effective Dart warns against
Before you start
What you’ll learn · Before you start
What you will learn
- Apply the Effective Dart style, documentation, usage and design rules
- Implement the Strategy, State and Repository patterns the Dart way
- Choose a language feature over a pattern when it removes code
- Recognise the overused constructs that Effective Dart warns against
Before you start
On this page
Which classic design patterns does Dart code still need, once the language has sealed classes, function types and immutable values? The answer starts with Effective Dart: The Dart team's style guide, in four parts (Style, Documentation, Usage, Design), written as DO, DON'T, PREFER, AVOID and CONSIDER rules; package:lints encodes many of them for the analyzer., the style guide the Dart team writes against its own libraries, where every rule starts with one of five words: DO, DON’T, PREFER, AVOID or CONSIDER. Two tools apply much of it for you: dart format fixes the layout, and dart fix rewrites code to follow rules the analyzer knows.
This lesson runs the tools on code that breaks the rules, then writes Strategy and Repository the Dart way. Later parts cover State, immutable values, the constructs to stop reaching for, and patterns that build objects.
The style guide, and the tools that apply it
Effective Dart’s four guides cover layout and naming (Style), doc comments (Documentation), everyday code (Usage) and public APIs (Design). A few rules you meet at once: class names are UpperCamelCase and other identifiers lowerCamelCase; every flow-control statement uses curly braces; and dart: imports come before package: imports. The lints that encode many of these rules are turned on by one line in analysis_options.yaml: include: package:lints/recommended.yaml. The session runs dart analyze and dart fix on a file that breaks several rules.
// Before: against Effective Dart. dart fix rewrites the parts a lint can.
class box {
var value;
box(this.value);
int get doubled {
return value * 2;
}
}
void main() {
var b = new box(21);
print(b.doubled);
} """Shows dart analyze naming Effective Dart problems, then dart fix applying the rules it can."""
from session import files, run
files("style")
run("""
cd style
dart pub get --offline | tail -1
# The lints name the style and usage problems
dart analyze
# dart fix applies the fixes that lints provide
dart fix --apply
sed -n 2,12p bin/box.dart
""", pub=True) Output
$ cd style
$ dart pub get --offline | tail -1
Changed 1 dependency!
# The lints name the style and usage problems
$ dart analyze
Analyzing style...
info - bin/box.dart:2:7 - The type name 'box' isn't an UpperCamelCase identifier. Try changing the name to follow the UpperCamelCase style. - camel_case_types
info - bin/box.dart:3:7 - An uninitialized field should have an explicit type annotation. Try adding a type annotation. - prefer_typing_uninitialized_variables
info - bin/box.dart:3:7 - Missing type annotation. Try replacing 'var' with a type annotation. - strict_top_level_inference
info - bin/box.dart:11:11 - Unnecessary 'new' keyword. Try removing the 'new' keyword. - unnecessary_new
4 issues found.
# dart fix applies the fixes that lints provide
$ dart fix --apply
Computing fixes in style...
Applying fixes...
bin/box.dart
unnecessary_new - 1 fix
1 fix made in 1 file.
$ sed -n 2,12p bin/box.dart
class box {
var value;
box(this.value);
int get doubled {
return value * 2;
}
}
void main() {
var b = box(21);
print(b.doubled);
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 style_session.py
dart analyze named four problems: the lowercase class name (camel_case_types), the untyped field (prefer_typing_uninitialized_variables and strict_top_level_inference) and the redundant new (unnecessary_new). dart fix --apply repaired the one with an automatic fix, unnecessary_new, and left the name and the type, because renaming a class and choosing a type need a person. That is the split to remember: the tools settle the mechanical rules, and the style guide is worth reading for the ones that need judgement, such as a doc comment whose first sentence stands alone, starting with a noun phrase for a variable and with a third-person verb for a method that does something.
Answer the question above: the table then marks what fits your case and says why the rest does not.
| Tool | Layout and spacing | Lint rules | Names and types |
|---|---|---|---|
| dart format Run as: reflows the code | Good reflows braces, indent and line breaks | Poor does not read lint rules | Poor never renames or types anything |
| dart analyze Run as: reports lint findings | Mixed does not change layout, only reports | Good names every enabled lint it finds | Mixed reports a bad name, cannot fix it |
| dart fix Run as: applies lint fixes | Poor leaves layout to dart format | Good applies the lints that carry a fix, such as unnecessary_new | Poor a rename or a type needs a person |
How the answers decide
- “What do you want to settle?” Spacing and line breaks: rules out dart analyze and dart fix. Layout is dart format's job; the other two leave spacing alone.
- “What do you want to settle?” Lint rules with an automatic fix: rules out dart format. dart analyze finds the lints and dart fix applies the ones that carry a fix.
- “What do you want to settle?” A name or a type: rules out dart format and dart fix. Renaming a class or choosing a type needs a person; analyze only points at it.
Key idea
Run dart format and dart fix on every commit: they settle the mechanical rules of Effective Dart, which leaves you the ones that need judgement, names, types and API shape.
Checkpoint
Check: The tools that apply Effective Dart
A pattern is often a language feature in Dart
Many of the classic design patterns were written for languages where a function is not a value and a type hierarchy is the only tool. Dart has first-class functions, so several patterns shrink to a few lines. The Strategy pattern: A design pattern that makes a behaviour interchangeable. In Dart it is usually a function type rather than an interface with a subclass for each behaviour. pattern, a family of interchangeable behaviours, is the clearest: instead of a Discount interface with a subclass each, a discount is a Discount function, and percent returns one.
// A discount is a function, not a subclass: the Strategy pattern, written the Dart way.
typedef Discount = int Function(int paise);
int none(int paise) => paise;
int flat50(int paise) => paise - 5000 < 0 ? 0 : paise - 5000;
Discount percent(int off) =>
(paise) => paise - paise * off ~/ 100;
int priceWith(int paise, Discount discount) => discount(paise);
void main() {
const cartPaise = 120000; // 1,200.00 in paise
final rules = <String, Discount>{
'no offer': none,
'flat 50 off': flat50,
'10 percent': percent(10),
'20 percent': percent(20),
};
rules.forEach((name, discount) {
print('$name: ${priceWith(cartPaise, discount)} paise');
});
} Output
no offer: 120000 paise flat 50 off: 115000 paise 10 percent: 108000 paise 20 percent: 96000 paise
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run pricing.dart
Each rule is a function of one type, kept in a map and applied by calling it, with no class to implement; percent(10) is a closure over the rate. Observer is the same story told with Stream, and Command with a function you store and call later.
The Repository pattern: A design that hides where data is stored behind an interface, so the code that uses it does not depend on a database; in Dart the interface is an abstract interface class. pattern does need a type, because it names a capability with several methods, and Dart’s way to name a capability is an abstract interface class. The service below depends on OrderStore, not on a database, and receives one through its constructor, which is dependency injection: Giving an object the things it depends on from outside, usually through its constructor, instead of letting it create them, so a test can pass a fake and production a real one. without a framework. A fake store with the same interface makes the service testable.
repo/lib/orders.dart
/// An order a service stores and reads back.
class Order {
const Order(this.id, this.paise);
final String id;
final int paise;
}
/// What the service needs from storage, named as an interface class so callers depend on the shape, not a database.
abstract interface class OrderStore {
Future<void> save(Order order);
Future<Order?> byId(String id);
Future<int> total();
}
/// The service takes its store as a constructor parameter: dependency injection, no global state.
class OrderService {
OrderService(this._store);
final OrderStore _store;
Future<void> place(String id, int paise) {
if (paise <= 0)
throw ArgumentError.value(paise, 'paise', 'must be positive');
return _store.save(Order(id, paise));
}
Future<int> totalFor(List<String> ids) async {
var sum = 0;
for (final id in ids) {
final order = await _store.byId(id);
if (order != null) sum += order.paise;
}
return sum;
}
} repo/lib/memory_store.dart
import 'orders.dart';
/// A fake store for tests: a Map instead of a database, with the same interface.
class InMemoryOrderStore implements OrderStore {
final _orders = <String, Order>{};
@override
Future<void> save(Order order) async => _orders[order.id] = order;
@override
Future<Order?> byId(String id) async => _orders[id];
@override
Future<int> total() async =>
_orders.values.fold<int>(0, (sum, order) => sum + order.paise);
} repo/test/orders_test.dart
import 'package:repo_demo/memory_store.dart';
import 'package:repo_demo/orders.dart';
import 'package:test/test.dart';
void main() {
late OrderService service;
late InMemoryOrderStore store;
setUp(() {
store = InMemoryOrderStore();
service = OrderService(store);
});
test('place stores an order the service can total', () async {
await service.place('a', 40000);
await service.place('b', 25000);
expect(await service.totalFor(['a', 'b']), 65000);
});
test('place rejects a non-positive amount', () {
expect(() => service.place('c', 0), throwsArgumentError);
});
test('a missing id is skipped, not counted', () async {
await service.place('a', 40000);
expect(await service.totalFor(['a', 'missing']), 40000);
});
} """Analyses and tests the repository package: an interface, a fake store and constructor injection."""
from session import files, run
files("repo")
run("""
cd repo
dart pub get --offline | tail -1
dart analyze
dart test
""", pub=True) Output
$ cd repo $ dart pub get --offline | tail -1 Changed 49 dependencies! $ dart analyze Analyzing repo... No issues found! $ dart test --:-- +0: loading test/orders_test.dart --:-- +0: test/orders_test.dart: place stores an order the service can total --:-- +1: test/orders_test.dart: place rejects a non-positive amount --:-- +2: test/orders_test.dart: a missing id is skipped, not counted --:-- +3: All tests passed!
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 repo_session.py
The tests pass against the in-memory store, and a real store would implement OrderStore without changing a line of OrderService: the service is written and tested once, and storage is a parameter. abstract interface class says the type is meant to be implemented, not extended, which is one of the class modifiers Effective Dart’s Design guide recommends for a public API’s intent.
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- The service names what it needs. OrderService takes an OrderStore in its constructor, an abstract interface class with save, byId and total. It never names a database.
- A test passes a fake. The test makes an InMemoryOrderStore, a Map behind the same interface, and hands it to OrderService. No database, no setup.
- The service calls the interface. place and totalFor call save and byId on the store through the interface, so the service cannot tell a fake from a real one.
- Production passes a database. In production the same constructor receives a store backed by a database client. OrderService is unchanged; only the argument differs.
Inject the interface, not the implementation
OrderService(this._store) takes an OrderStore, so a test passes a fake and production passes a database client. A service that built its own store inside the constructor could not be tested without one.
Checkpoint
Check: Patterns as functions and interfaces
Practise
Start with dart create -t package patterns_practice, add lints and test as dev dependencies, and turn on package:lints/recommended.yaml in analysis_options.yaml.
- Write three shipping rules as functions of one
ShippingRuletype, keep them in a map, and apply each to a cart. - Write a
Clockinterface with anow()method, a fake that returns a fixed time, and a function that takes aClockand greets by the hour; test it with the fake. - Run
dart analyzeanddart fix --applyon code that usesnewand misnames a class, and read what changed.
Practice that adapts to you
Practise this lesson
3 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
State as sealed classes, immutability by default, and what to stop reaching for Pro: opens with a Pro, Premium or Ultimate pass
The patterns a Dart codebase leans on hardest are the ones the language makes cheap: a closed set of states, and values that never change. This part writes both, then names the constructs Effective Dart’s Usage and Design guides tell you to avoid, with the feature that replaces each.
In this part:
- A closed set of states with a sealed class
- Immutability by default
- What Effective Dart tells you to stop reaching for
3 runnable examples (Dart) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; nothing renews unless you choose to. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Building, creating and adapting objects with the language instead of ceremony Pro: opens with a Pro, Premium or Ultimate pass
Four more patterns from the classic catalogue are about making objects: a builder for many options, a factory that decides what to return, one shared instance, and an adapter over an API you cannot change. Each has a short Dart form, and one program runs all four.
In this part:
- Builder, factory, singleton and adapter in one program
- Design rules that keep these APIs readable
- When the classic form is still the right one
- What to keep from the two parts
1 runnable example (Dart) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; nothing renews unless you choose to. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
Three questions in the module’s bank ask you to weigh a pattern against a language feature and defend a style choice:
-
Mid to senior
A teammate ports a Java codebase to Dart and keeps every Gang-of-Four pattern as its own class hierarchy: a Strategy interface with a subclass per rule, a Subject and Observer, a State base class. Which would you simplify, and how do you explain the Dart version?
-
Mid to senior
A service parses JSON into Map<String, dynamic> and passes those maps deep into the code, reading fields as dynamic everywhere. Bugs show up only in production. What is wrong, by Effective Dart, and how do you change it?
-
Mid to staff
A screen's state is a mutable class with nullable fields for loading, data and error, and bugs come from fields that disagree, such as data set while an error is also set. Redesign the state the Dart way and say what it prevents.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
With which five words does every Effective Dart rule start?
DO, DON'T, PREFER, AVOID and CONSIDER. Its four guides cover Style (layout and naming), Documentation, Usage and Design (public APIs).
-
Which Effective Dart rules do you meet at once?
Class names in UpperCamelCase and other identifiers in lowerCamelCase, curly braces on every flow-control statement, and dart: imports before package: imports.
-
Which Effective Dart problems does dart fix leave to you?
The ones that need judgement: it removed a redundant new (unnecessary_new) but left a lowercase class name and an untyped field, because renaming a class and choosing a type need a person.
-
How is the Strategy pattern usually written in Dart?
As a function type: each discount rule is a function, and percent(10) returns a closure over its rate. Observer becomes a Stream, and Command a function you store and call later.
-
How is a Repository written in Dart?
As an abstract interface class, such as OrderStore, that the service receives through its constructor. A fake store with the same interface makes the service testable without a database.
-
Why should OrderService take an OrderStore in its constructor instead of building one?
A test can then pass a fake and production a database client; a service that built its own store could not be tested without one.
Key takeaways
- Effective Dart writes its rules as DO, DON’T, PREFER, AVOID and CONSIDER, and
dart formatanddart fixapply the mechanical ones. - Strategy is a function type, Observer a
Streamand Command a stored function. - Repository stays an
abstract interface class, injected through the constructor so a fake can stand in.
Terms in this lesson
- dependency injection
- Giving an object the things it depends on from outside, usually through its constructor, instead of letting it create them, so a test can pass a fake and production a real one.
- Effective Dart
- The Dart team's style guide, in four parts (Style, Documentation, Usage, Design), written as DO, DON'T, PREFER, AVOID and CONSIDER rules; package:lints encodes many of them for the analyzer.
- Repository pattern
- A design that hides where data is stored behind an interface, so the code that uses it does not depend on a database; in Dart the interface is an abstract interface class.
- State pattern
- A design where a value is in exactly one of a set of states; in Dart a sealed class names the states and an exhaustive switch handles each one.
- Strategy pattern
- A design pattern that makes a behaviour interchangeable. In Dart it is usually a function type rather than an interface with a subclass for each behaviour.
References
- Effective Dart (The Dart project (Google))
- Effective Dart: Style (The Dart project (Google))
- Effective Dart: Documentation (The Dart project (Google))
- Effective Dart: Usage (The Dart project (Google))
- Effective Dart: Design (The Dart project (Google))
- dart:core List class (The Dart project (Google))
Report a problem with this lesson
Kept only in this browser. Your Learn progress