In a Flutter federated plugin, why must platform implementations extend the PlatformInterface subclass rather than implement it, and how do tests still mock it?
answer
- adding a method must stay non-breaking
- private token passed to super
- instance setter verifies the token
- AssertionError on implements
- MockPlatformInterfaceMixin, debug builds only
basics
~20 sExtending 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 sThe 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
Recall that platform implementations extend the interface class and that tests use MockPlatformInterfaceMixin to plug in fakes.
Explain the private token, the instance setter's verify call and the AssertionError a class using implements triggers.
Connect the rule to semantic versioning: new methods with throwing defaults are minor releases, while removals break every platform package.
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.