Federated Plugin Architecture

FlutterDartPluginsMobile
Share on LinkedIn

I maintained a plugin where someone opened a PR adding Windows support inside the same repo as Android. CI broke iOS because the Windows CMake touched shared headers. Federated architecture would have put Windows in its own package with its own pipeline, leaving mobile releases alone.

A federated Flutter plugin is a family of packages: an app-facing facade, a platform interface, and one implementation package per platform (plus optional endorsements).

Package roles

my_plugin/                  # app-facing API users import
my_plugin_platform_interface/  # abstract contract + token verification
my_plugin_android/
my_plugin_ios/
my_plugin_web/

Apps depend only on my_plugin. That package exports a simple API and delegates to the platform interface singleton.

Platform interface pattern

// my_plugin_platform_interface
abstract class MyPluginPlatform extends PlatformInterface {
  MyPluginPlatform() : super(token: _token);
  static final Object _token = Object();
  static MyPluginPlatform _instance = MethodChannelMyPlugin();

  static MyPluginPlatform get instance => _instance;

  static set instance(MyPluginPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String> getPlatformVersion();
}

Implementations extend the class and call super() so token verification passes.

App-facing package

// my_plugin
class MyPlugin {
  Future<String> getPlatformVersion() {
    return MyPluginPlatform.instance.getPlatformVersion();
  }
}

pubspec.yaml endorses default implementations:

dependencies:
  my_plugin_platform_interface: ^1.0.0
  my_plugin_android: ^1.0.0
  my_plugin_ios: ^1.0.0

flutter:
  plugin:
    platforms:
      android:
        default_package: my_plugin_android
      ios:
        default_package: my_plugin_ios

Pub pulls endorsed packages transitively—users run flutter pub add my_plugin and get mobile implementations without listing each.

Platform implementation package

// my_plugin_android
class MyPluginAndroid extends MyPluginPlatform {
  static void registerWith() {
    MyPluginPlatform.instance = MyPluginAndroid();
  }

  @override
  Future<String> getPlatformVersion() async {
    return 'Android ${android.os.Build.VERSION.release}';
  }
}

Register in pubspec.yaml plugin section for that platform only.

Testing with fakes

Tests swap the platform instance:

class FakeMyPlugin extends MyPluginPlatform {
  @override
  Future<String> getPlatformVersion() async => 'Fake 1.0';
}

void main() {
  setUp(() {
    MyPluginPlatform.instance = FakeMyPlugin();
  });
}

No method channel mocking required for unit tests of Dart logic above the interface.

When to federate vs monolith

Monolith plugin — one team, mobile-only, fast iteration, shared native code.

Federated — multiple contributors per platform, web/desktop arriving later, enterprise forks (e.g., custom Linux build for kiosks).

Migration path: extract interface first, move Android code to sub-package, keep old import path stable via export files.

Publishing and versioning

Version interface breaking changes as major bumps across all packages simultaneously—or use tight constraint ranges (my_plugin_platform_interface: ^2.0.0) and changelog coordination. Melos helps monorepo-style plugin families.

Document which platforms are community-maintained vs officially endorsed so issue triage lands in the right repo.

Common mistakes

Version coordination across packages

When my_plugin_platform_interface bumps major, all implementation packages must publish compatible ranges simultaneously. Melos or a monorepo prevents version skew:

# my_plugin_android pubspec
dependencies:
  my_plugin_platform_interface: ^2.0.0

Changelog in app-facing package summarizes breaking changes across family. Consumers should only import my_plugin—document that importing implementation packages directly voids support.

Web and desktop implementations

Federation shines when web team ships my_plugin_web without blocking mobile release. Endorse web optionally:

flutter:
  plugin:
    platforms:
      web:
        default_package: my_plugin_web

If web is experimental, omit endorsement—users add dependency explicitly knowing maturity.

Code generation inside implementations

Pigeon definitions live in interface package or app-facing package; implementations consume generated native stubs. Avoid duplicating Pigeon outputs—single generation step in CI.

Consumer override for tests

MyPluginPlatform.instance = FakeMyPluginPlatform();

Document in README for integrators writing tests. Reset instance in tearDown to prevent test pollution.

Governance

Assign CODEOWNERS per platform package in monorepo. Cross-platform API changes require review from each platform owner before merge—prevents Android-only breaks landing on iOS unchecked.

Real-world plugin family example

Study url_launcher on GitHub: app-facing API, platform interface, endorsed Android/iOS/web implementations. Notice how LinkTarget enum lives in interface package—platform code depends on shared types without circular imports.

When forking for enterprise (custom URL allowlist), fork interface + app package only; swap implementation package via pubspec dependency override in internal apps.

Release checklist per platform package

Stub implementations

Provide no-op stub for unsupported platforms returning UnimplementedError or graceful fallback—consumers check Platform.isX or catch at app level.

Plugin federation and Melos

Monorepo all packages with Melos bootstrap—single PR updates interface and all platforms atomically; ideal federation workflow.

Documentation site

Auto-generate platform support table in README from pubspec plugin declarations—script in CI updates checkmarks so docs never stale.

Breaking change communication

Major interface bump: publish migration guide with before/after snippets for each platform implementation team.

Platform channel removal

When migrating MethodChannel plugin to federated Pigeon, deprecate old channel with debug log warning one release—consumers upgrading see clear migration message in console.

Additional release coordination

Schedule federated plugin family releases Tuesday–Thursday mornings team timezone—avoid Friday deploys of native platform packages requiring store review correlation. Rollback plan pins previous version constraints in app-facing package pubspec within one hour if crash spike detected in Crashlytics tagged release version.

Version federated plugin interfaces independently — breaking interface change in platform interface package breaks all implementations simultaneously.

Resources

Frequently asked questions

What problem do federated plugins solve?

They decouple the app-facing API from platform-specific implementations so third parties can ship web or desktop support without forking the whole plugin. The core package defines the interface; platform packages register implementations.

What is an endorsed implementation?

When the main plugin re-exports a default platform package in its pubspec, pub resolves it automatically—users add one dependency and get Android, iOS, web, etc. Non-endorsed implementations require explicit dependency overrides for alternate vendors.

Do I need federation for a simple Android+iOS plugin?

No. Single-repo plugins with android/ and ios/ folders are fine for two mobile platforms. Federation pays off when you expect community platform packages, independent release cycles, or optional platform support.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →