skip to content

In a Flutter federated plugin, why must platform implementations extend the PlatformInterface subclass rather than implement it, and how do tests still mock it?

level: seniorimportance: should knowfreq 24%

answer

  1. adding a method must stay non-breaking
  2. private token passed to super
  3. instance setter verifies the token
  4. AssertionError on implements
  5. MockPlatformInterfaceMixin, debug builds only

basics

~20 s

Extending lets the interface package add new methods with default bodies without breaking any implementation; implementing would force every platform package to update. The instance setter enforces this with a token check, and tests opt out with MockPlatformInterfaceMixin.

solid answer

~40 s

The platform-interface class extends `PlatformInterface` from `plugin_platform_interface`, passes a private `Object()` token to `super(token: _token)`, and its static `instance` setter calls `PlatformInterface.verify(instance, _token)` (or the older, laxer `verifyToken`). A subclass created with `extends` runs that constructor and carries the right token; a class that uses `implements` never does, so setting it throws an `AssertionError` stating platform interfaces must not be implemented with `implements`. The reason is versioning: with `extends`, adding a method that defaults to `throw UnimplementedError()` is a **non-breaking** minor release, whereas every `implements` class would stop compiling. Mocks and fakes mix in `MockPlatformInterfaceMixin`, which passes the check only when assertions are enabled — in release it throws.

go deeper

for a junior

Recall that platform implementations extend the interface class and that tests use MockPlatformInterfaceMixin to plug in fakes.

for a middle

Explain the private token, the instance setter's verify call and the AssertionError a class using implements triggers.

for a senior

Connect the rule to semantic versioning: new methods with throwing defaults are minor releases, while removals break every platform package.

for a principal

Own the interface's change policy across maintainers, deciding when a breaking interface release is worth coordinating every implementation.

## The problem the rule solves In a federated plugin, many packages — some written by people who never talk to each other — depend on one abstract class in the **platform-interface package**. That class will grow: a new feature means a new method. In Dart: - A class that **extends** an abstract class inherits every concrete method body. If the interface adds `Future<double> readHumidity() { throw UnimplementedError(); }`, subclasses still compile and simply inherit the throwing default. - A class that **implements** it must declare every member itself. Adding any method breaks every implementing class until its author catches up. So the ecosystem's rule is: **platform interfaces grow by adding methods with default bodies, and implementations must extend, never implement**. That keeps additions a minor, non-breaking release and lets platform packages adopt new methods at their own pace. ## How the rule is enforced The `plugin_platform_interface` package turns the convention into a runtime check: ```dart abstract class ThermoPlatform extends PlatformInterface { ThermoPlatform() : super(token: _token); static final Object _token = Object(); static ThermoPlatform _instance = MethodChannelThermo(); static ThermoPlatform get instance => _instance; static set instance(ThermoPlatform instance) { PlatformInterface.verify(instance, _token); _instance = instance; } Future<double> readCelsius() { throw UnimplementedError('readCelsius() has not been implemented.'); } } ``` 1. The constructor hands a **private token** to `PlatformInterface`, which remembers it for that object (in an `Expando`). 2. Only code that runs this constructor — a subclass using `extends` — produces an object carrying the right token. 3. The `instance` setter calls `verify`. If the tokens do not match, it throws an `AssertionError` saying platform interfaces must not be implemented with `implements`. The check is an explicit `throw`, not an `assert`, so it applies in release builds too. | Method | Checks the token | Rejects `const Object()` as the token | |---|---|---| | `PlatformInterface.verify` | Yes | Yes | | `PlatformInterface.verifyToken` | Yes | No — documented as to be deprecated | `flutter create --template=plugin` still generates `verifyToken`; new interfaces can use `verify`. The token must be a fresh `Object()`, because every `const Object()` is the same canonical instance and would let any class forge it. ## Testing: mocks and fakes A mockito `Mock` or a `Fake` is usually written with `implements`, so it would fail verification. The package provides **`MockPlatformInterfaceMixin`**: ```dart class FakeThermoPlatform extends Fake with MockPlatformInterfaceMixin implements ThermoPlatform { @override Future<double> readCelsius() async => 21.5; } void main() { test('reads temperature', () async { ThermoPlatform.instance = FakeThermoPlatform(); expect(await Thermo().readCelsius(), 21.5); }); } ``` - Objects with the mixin pass `verify` **only when assertions are enabled** (tests and debug builds). - In a release build the mixin makes `verify` throw, so a test double can never ship as the real implementation. ## What this means for plugin maintainers - Add methods to the interface with bodies that `throw UnimplementedError('readHumidity() has not been implemented.')`. That is a minor version. - Removing or changing a method's signature is still breaking and forces every platform package to update — the most expensive change in a federated plugin, so it is avoided or batched. - Platform packages keep the default for methods their platform cannot support; the app-facing package can document or detect that. - Do not reach for `implements` to "decouple" an implementation; it trades a compile-time convenience for a runtime failure the first time the app sets `instance`. ## Common misunderstandings - The check is not a lint; it is enforced when `instance` is assigned. - `extends` does not mean implementations share state with the interface; each registers its own object. - The mixin is not a production escape hatch.

  • Why must the token be a new Object() rather than const Object()?
    Every `const Object()` expression evaluates to the same canonical instance, so any class could pass it and defeat the check. `PlatformInterface.verify` therefore throws if the stored token is `const Object()`; the older `verifyToken` skips that extra check.
  • What does a platform package do with an interface method its platform cannot support?
    It inherits the default body, which throws `UnimplementedError`. Because the package extends the interface, it still compiles after the method is added, and the app-facing package can document the gap or catch the error.
  • Is removing a method from a platform interface a minor release?
    No. Removing or changing a signature breaks every platform package that overrides or calls it, so it is a breaking change to the interface package. Maintainers avoid it or batch such changes, because every implementation must be updated in step.

saying these in an interview costs you the question

  • The extends rule is only a style lint with no runtime effect.
  • Using implements is safer because it avoids inheriting behaviour.
  • A const Object() token works just as well as a new Object().
  • MockPlatformInterfaceMixin can be used to swap implementations in production.
  • Adding any method to a platform interface is always a breaking change.