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.
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.
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);
}
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.
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);
}
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.