Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 15 minutes
  • Examples run with dart 3.13.5 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: The style guide, and the tools that apply it · A pattern is often a language feature in Dart · Practise. With a pass: 2 in-depth parts and 3 interview answers, about 10 minutes.

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.

bin/box.dart: a lowercase class name, an untyped field and a needless new Dart · style/bin/box.dart
// 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);
}
dart analyze names the problems, dart fix repairs what it can Python · style_session.py
"""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.

Interactive · answer and compare Which tool settles which rule? Each row is a kind of rule. The marks say whether a tool fixes it, reports it, or leaves it to you.
What do you want to settle?

Answer the question above: the table then marks what fits your case and says why the rest does not.

The three dart tools against the kinds of Effective Dart rule, from the style session
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

  1. Question 1 of 2 A class is named box and the code uses new. Which tool renames the class to Box?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: None; dart analyze reports it but a rename needs a person

    The tools settle layout and the lints that carry a fix; names and types are left to you.

  2. Question 2 of 2 Effective Dart orders imports how?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: dart: imports first, then package: imports

    dart:, then package:, then relative, and the lint directives_ordering applies it.

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.

The Strategy pattern as function types Dart · pricing.dart
// 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.

A repository interface, a fake store and constructor injection

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);
  });
}
dart analyze and dart test on the repository package Python · repo_session.py
"""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.

Interactive · message by message One service, a fake in tests and a database in production Step through how OrderService receives its store, so the same code runs against a fake and a real database.
One service, a fake in tests and a database in production, step 4 of 4. Messages so far: A test to Service: new OrderService(fake); Service to Store: save, byId.A testServiceStorea databasenew OrderService(fake)save, byId

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. Question 1 of 2 In the pricing example, what is a discount?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A function of type int Function(int)

    Strategy is a family of interchangeable behaviours, which in Dart is a function type.

  2. Question 2 of 2 Why does OrderService take an OrderStore in its constructor instead of making its own?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: So a test can pass a fake store and production a real one, with the service unchanged

    Depend on the interface and receive it through the constructor, which is dependency injection without a framework.

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.

  1. Write three shipping rules as functions of one ShippingRule type, keep them in a map, and apply each to a cart.
  2. Write a Clock interface with a now() method, a fake that returns a fixed time, and a function that takes a Clock and greets by the hour; test it with the fake.
  3. Run dart analyze and dart fix --apply on code that uses new and 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.

  1. Question 1 of 3 You need the Observer pattern: parts of the app react when an order changes. What is the Dart way?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: Expose a Stream the listeners subscribe to

    Observer is a Stream; Strategy is a function; Command is a stored function. Reach for the feature first.

  2. Question 2 of 3 Why is OrderStore declared abstract interface class rather than plain abstract class?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: To say it is meant to be implemented, not extended, outside its library

    interface says implement me; final says do neither; sealed says I know all my subtypes.

  3. Question 3 of 3 Which pair of commands belongs in a pre-commit step for Effective Dart?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: dart format and dart fix --apply

    The tools cover the mechanical rules, leaving names, types and API shape to review.

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.

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.

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:

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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).

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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 format and dart fix apply the mechanical ones.
  • Strategy is a function type, Observer a Stream and 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.