Plugin Methodology for Desktop: Shipping Flutter Plugins for Windows, Linux, and macOS
What changes when a Flutter plugin targets desktop: CMake and podspec files, endorsed federated implementations, dart-only plugins, testing, and when dart:ffi wins.
Published on • November 10, 2026
AI Assistant

A plugin that works on Android and iOS can silently ship with no desktop support at all, because platform support is declared per platform rather than inherited. This post walks through what actually changes when the same plugin targets Windows, Linux, and macOS: the pubspec platform map, native build files, federated and endorsed implementations, testing, and the moment you should skip plugin machinery entirely and reach for dart:ffi.
Choose the package type before you scaffold
The Flutter docs sort packages into three kinds: plain Dart packages, plugin packages (a Dart API plus one or more native implementations), and FFI packages (Dart calling native code through dart:ffi, with no OS-specific build files). Scaffold the plugin explicitly listing desktop targets:
flutter create --org com.example --template=plugin \
--platforms=android,ios,linux,macos,windows -a kotlin hello
If you pass no --platforms, the generated project supports no platforms at all, and to bolt desktop onto an existing plugin you run the same command again inside the project directory (flutter create --template=plugin --platforms=windows,linux .). The result declares each target in the flutter.plugin.platforms map, and desktop entries use the same pluginClass key as mobile:
flutter:
plugin:
platforms:
windows:
pluginClass: HelloPlugin
linux:
pluginClass: HelloPlugin
macos:
pluginClass: HelloPlugin
Where desktop platform code actually lives
Each desktop target keeps its sources in its own folder and its own build system:
- Windows: C++ under
windows/(.h+.cpp), built by CMake. Edit it through the generatedbuild/windows/<app>_example.slnin Visual Studio, and always Build > Build Solution before running again or you will test a stale copy of the plugin. - Linux: C++ under
linux/(.h+.cc), also CMake. The sources appear underflutter/ephemeral/.plugin_symlinks/hello/linux/, and creating a runnable app requires Flutter tool steps, so building straight from your editor’s CMake integration will not work. - macOS: Swift under
macos/, built with Swift Package Manager as the primary dependency strategy (createios/my_plugin_name/Package.swiftor themacos/equivalent) with CocoaPods still supported through the.podspecfor older projects.
Before editing any of them, build the example once so the generated project files exist:
cd hello/example
flutter build windows
flutter build linux
flutter build macos --config-only
Two macOS/Windows details that save a lot of pain: sharedDarwinSource: true lets iOS and macOS share one darwin/ folder instead of duplicating code, and the podspec must then declare both s.ios.dependency 'Flutter' and s.osx.dependency 'FlutterMacOS' with their own deployment targets.
Federated and endorsed desktop implementations
Federated plugins split the package into an app-facing interface, a platform interface that any implementation must implement, and one or more platform implementations. A desktop implementation package declares which app-facing package it implements:
# pubspec.yaml of hello_windows
flutter:
plugin:
implements: hello
platforms:
windows:
pluginClass: HelloPlugin
An implementation is endorsed when the app-facing author adds it as a dependency and lists it as default_package, so apps get it automatically:
flutter:
plugin:
platforms:
windows:
default_package: hello_windows
dependencies:
hello_windows: ^1.0.0
Without endorsement, a developer must add hello_windows manually to their own pubspec.yaml — which also doubles as a clean way to override an endorsed implementation. This is how a domain expert can ship support for a platform the original author never touched.
Dart-only and hybrid desktop implementations
Not every desktop plugin needs C++. When an implementation is pure Dart (typically FFI-backed), swap pluginClass for dartPluginClass:
flutter:
plugin:
implements: hello
platforms:
windows:
dartPluginClass: HelloPluginWindows
class HelloPluginWindows extends HelloPluginPlatform {
/// Called during startup; registers this as the default instance.
static void registerWith() {
HelloPluginPlatform.instance = HelloPluginWindows();
}
}
Listing both keys produces a hybrid: a Dart class for Dart-side registration plus a native class for the platform channel. Note the docs’ warning that every engine creates its own plugin instance, so keep state on the instance, not in statics, and clean up when the engine detaches.
Testing desktop plugins
The docs recommend three layers, and desktop adds its own native harness:
flutter test # Dart unit + widget tests (mock channels)
cd example && flutter test integration_test # Dart <-> native round trip
Unit tests never load native code, so platform channels must be mocked. Integration tests run inside the example app and are the only place that exercises the real channel. Native unit tests in the template use GoogleTest: build the example first, then run build/linux/plugins/x64/debug/hello/hello_test on Linux or build/windows/plugins/hello/Debug/hello_test.exe on Windows. Aim for at least one integration test per platform channel call.
When not to write a plugin: use package_ffi
If you only need to call a C library and do not need the Flutter Plugin API or Google Play services configuration, do not build a plugin. Since Flutter 3.38 the recommended path is an FFI package:
flutter create --template=package_ffi hello
That template ships lib/, native src/ sources, a hook/build.dart build hook that compiles them, and an ffigen.yaml for generating bindings from the header (dart run tool/ffigen.dart). It works for both Flutter and Dart standalone, with no per-OS build files. On Windows specifically, before writing any native code check whether an existing Dart package already wraps the API — the docs point at win32 for Win32/COM access, with many community packages building on it. Write the plugin only when you need platform channels, custom native UI, or a library that is not already reachable from Dart.
Takeaway: Desktop support in a plugin is opt-in per platform and comes with its own build files (CMake for Windows and Linux, SwiftPM/CocoaPods for macOS), so declare targets explicitly, split platform implementations into endorsed federated packages when they grow, test each channel with an integration test plus GoogleTest, and skip plugins altogether for plain C libraries by using the package_ffi template.
Sources
- https://docs.flutter.dev/packages-and-plugins/developing-packages
- https://docs.flutter.dev/tools/pubspec
- https://docs.flutter.dev/testing/testing-plugins
- https://docs.flutter.dev/testing/plugins-in-tests
- https://docs.flutter.dev/interop/bind-native-code
- https://docs.flutter.dev/platform-integration/windows/building