Course outline

Dart / Contracts: patterns, generic APIs, and asynchronous results / Lesson 10

Choose API boundaries, mixins, and static extensions

Distinguish implementation inheritance, interface contracts, reusable mixin behavior, and extension lookup under Dart library rules.

About 5 min readContent reviewed
On this page

Lesson goals

  • Select a class modifier according to the extension and implementation rights the API intends to expose.
  • Explain why an extension is not a virtual override and a mixin is not resource ownership.

Modifiers describe rights across the library boundary

A Dart class normally exposes an implicit interface, so extension and implementation are separate ways to depend on it. Dart 3 class modifiers make intended rights clearer. An abstract interface class describes a contract that cannot be instantiated and can be implemented outside its library. interface prevents external extension; final prevents external extension and implementation; base requires implementation inheritance rather than external implementation and imposes related subtype modifier rules. sealed restricts direct alternatives to its library. Read the exact modifier combination rather than assuming final means every field is immutable or that every restriction applies identically inside and outside the library.

Reuse a coherent behavior with explicit required members

A mixin supplies reusable instance behavior without being an ordinary generative-constructor class. It can declare abstract members that the applying class must provide; an on constraint can express an appropriate superclass requirement when needed. The example requires an id getter and derives a caption. It also implements a small interface so the capability and the reused implementation remain distinguishable. Avoid using a large mixin as an invisible bag of unrelated state and methods. Review method conflicts, application order, and dependencies before combining several mixins in one class.

dart An interface contract plus a reusable mixin implementation
abstract interface class BatchInfo {
  String get id;
}
mixin Captioned {
  String get id;
  String get caption => 'batch:$id';
}
final class DisplayBatch with Captioned implements BatchInfo {
  @override
  final String id;
  DisplayBatch(this.id);
}
void main() {
  final batch = DisplayBatch('B-7');
  print(batch.id);
  print(batch.caption);
}
Expected output
B-7
batch:B-7

Expected output is B-7 then batch:B-7. The final field supplies the required getter. Dart 3 modifier support is required, and no compilation or runtime test was performed.

Inheritance and reuse do not settle lifecycle policy

A mixin that calls a logger still depends on that logger’s behavior, and an interface that mentions close does not decide who owns the resource. Document whether the caller or object closes a dependency, what can fail, and whether methods block or schedule asynchronous work. Prefer composition when an object merely uses a capability rather than satisfying a real substitution contract. Implementing an implicit interface supplies the required public members but does not inherit private storage or method bodies. Concise syntax should not conceal a different meaning behind a familiar method name.

Extension selection uses the static receiver type

An extension makes functions available with member-like syntax without modifying the original class. It cannot gain private access to another library merely because the call resembles a member. Extensions are resolved statically, and dynamic receivers do not discover them as runtime members. Applicable instance members take priority over extensions. The sample passes a SpecialMarker through a Marker parameter, so the function selects the Marker extension. If behavior must vary by the runtime subtype, use a genuine instance contract with overriding instead of expecting extensions to provide virtual dispatch.

dart Static extension selection through a parameter boundary
class Marker {}
class SpecialMarker extends Marker {}
extension MarkerLabel on Marker {
  String get label => 'base extension';
}
extension SpecialLabel on SpecialMarker {
  String get label => 'special extension';
}
String readLabel(Marker value) => value.label;
void main() {
  print(readLabel(SpecialMarker()));
  print(SpecialMarker().label);
}
Expected output
base extension
special extension

Expected output is base extension then special extension. The object subtype alone does not select the first extension. This complete source is illustrative and uncompiled.

Review imports and extension conflicts as API changes

Two imported extensions can make a call ambiguous, and adding an instance member can change which declaration is used. Named extension invocation or import controls can make intent explicit when resolving a conflict. Do not hide network I/O or destructive mutation behind an innocent-looking extension getter. Document effects, validate boundary data, and choose names that communicate whether a helper creates a value or mutates an owner. When evolving an API, review external implementers and extenders against its modifier contract. A source change that works inside one library may still break dependent libraries with different rights.

Common errors and fixes

final class is assumed to make every instance deeply immutable.

Why it happens
Subtype restrictions were confused with field and object mutation.
How to fix it
Review field mutability and nested ownership independently.
Prevent it next time
State the exact guarantee of each class modifier.

A dynamic receiver cannot call the expected extension helper.

Why it happens
Static extension lookup was mistaken for runtime member dispatch.
How to fix it
Use a known static type or a real instance interface as appropriate.
Prevent it next time
Test callers at the actual declared receiver boundary.

A mixin silently depends on an unavailable capability.

Why it happens
Required members or superclass constraints were left implicit.
How to fix it
Declare the requirement explicitly or pass the dependency through composition.
Prevent it next time
Review every reused behavior’s effects and lifecycle needs.

Check your understanding

1. Should an external client implement a base class merely because it can list all public methods?

Show worked solution

No. The modifier intentionally restricts external implementation and preserves implementation-inheritance guarantees. Choose the API’s supported extension route or depend on an explicitly implementable interface. Reproducing signatures does not grant rights that the library author withheld, and it may bypass hidden assumptions the base implementation maintains.

2. Why is a static extension a poor replacement for a polymorphic formatter?

Show worked solution

Its selection follows the declared receiver type and imports, so a broad parameter can choose different behavior than the runtime subtype suggests. A formatter that must vary by implementation should expose an instance operation or a separately injected formatting strategy. Use extensions for clear local convenience while preserving the real behavior contract.

Key takeaways

  • Class modifiers, mixins, and extensions control different aspects of an API.
  • Reuse syntax does not create ownership, immutability, or virtual behavior automatically.

Official references