How to Build Custom Flutter Plugins

Want to connect your Flutter app to native features like cameras or GPS? Custom Flutter plugins are the solution. These plugins act as a bridge between Flutter’s Dart code and native platform functionality (e.g., Android, iOS, Windows). Here’s a quick breakdown of what you need to know:
- What are Flutter Plugins?
They’re packages that link Flutter apps to native device capabilities using Platform Channels. Plugins combine Dart APIs with native code (e.g., Kotlin for Android, Swift for iOS). - When Do You Need Custom Plugins?
When existing plugins don’t meet your requirements – like integrating proprietary SDKs or accessing unsupported native features. - What Do You Need?
- Flutter SDK and CLI
- Dart programming skills
- Native coding knowledge (Kotlin/Java for Android, Swift/Objective-C for iOS, etc.)
- Understanding of Platform Channels for communication between Dart and native code
- How to Start?
Use the Flutter CLI:
flutter create --template=plugin --platforms=android,ios <plugin_name>
This generates a project structure with folders for Dart code, native implementations, and an example app for testing. - Testing and Publishing:
Test your plugin using the example app and automated tests. Once it’s ready, publish it on pub.dev following Flutter’s guidelines.
Creating custom plugins gives you complete control over how your Flutter app interacts with native features. This guide walks you through the setup, coding, testing, and publishing process.

How to Build and Publish Custom Flutter Plugins: Complete Workflow
Setting Up Your Plugin Project
Once you’ve grasped the purpose and importance of creating custom plugins, it’s time to dive into setting up your development project.
Creating a Plugin with Flutter CLI

To create a custom plugin, use the Flutter CLI. The key command to get started is:
flutter create --template=plugin <project_name>
This command generates a Dart package with platform-specific implementation code for your plugin’s functionality.
If your plugin needs to support specific platforms, include the --platforms flag. For example, to target Android and iOS, run:
flutter create --template=plugin --platforms=android,ios hello
If you skip the --platforms flag, your project won’t support any platform by default. You also need to set an organisation ID using the --org flag, formatted in reverse domain notation (e.g., com.example). This ID becomes part of your Android package name and iOS bundle identifier, so double-check its accuracy from the start.
By default, Flutter plugins use Kotlin for Android and Swift for iOS. If you prefer Java or Objective-C, you can switch using the -a and -i flags respectively.
After creating your plugin, it’s a good idea to verify the setup by running the example app. For instance, use the command flutter build apk --config-only in the example folder to check the configuration before diving into native code. If you later decide to expand your plugin’s reach to another platform, like web, you can add support by running:
flutter create --template=plugin --platforms=web .
This command integrates the new platform into your existing project directory.
With the basic structure in place, take a moment to review the directory layout to understand where you’ll implement the core functionality.
Plugin Directory Structure Explained
The generated project structure is designed to neatly organise your plugin’s Dart API, native code, and example app. Here’s a quick breakdown of the key components:
lib/folder: This is where your Dart API resides. It’s the public-facing interface developers will use when they import your plugin. Modern plugins often include Dart files for the platform interface, method channel, and public API.android/andios/directories: These folders contain platform-specific implementations. By default, Android uses Kotlin, while iOS uses Swift. They also include essential build scripts – Gradle for Android and CocoaPods for iOS.example/folder: This folder is a complete Flutter app that depends on your plugin. It’s perfect for manual testing and showcasing your plugin’s functionality.pubspec.yaml: This file is crucial for your plugin. Beyond the usual metadata, it includes aflutter: plugin:section that declares supported platforms and specifies the entry classes for each.
As Innocencia Ndembera aptly describes it:
“Think of pubspec.yaml as a shopping list for your project, and pubspec.lock as the receipt that shows what you bought at the store, ensuring that your project consistently uses the same versions of packages”.
Implementing Plugin Functionality
Once your plugin structure is ready, the next step is connecting the Dart interface to the native platform code. This is where your plugin bridges Flutter’s framework with Android and iOS capabilities, bringing it to life.
Writing the Dart API

The Dart API is defined in the lib/<plugin_name>.dart file and acts as the public-facing interface for developers. It enables communication with native code using the MethodChannel class, which handles asynchronous calls between Dart and the underlying platforms.
To set this up:
- Create a
MethodChannelwith a unique, domain-prefixed name (e.g.,com.example.battery/methods) to avoid conflicts. - Use
invokeMethod<T>(whereTis the expected return type) to call native code. Wrap it in atry-catchblock to handlePlatformExceptionerrors gracefully. - Since
invokeMethodreturns aFuture<T?>, you can use the null-coalescing operator (e.g.,value ?? false) to provide a default value in case of null.
Data sent through the channel is automatically serialised using StandardMessageCodec, which supports JSON-like structures such as Maps, Lists, and primitives. Flutter ensures that messages are processed in the same order they are sent, maintaining FIFO (First In, First Out) ordering.
Once the Dart API is in place, the next step is to configure the native implementations.
Android Implementation

For Android, the native code should be placed in the android/src/main/kotlin directory. The main class here needs to implement two key interfaces:
FlutterPlugin: Manages the plugin’s lifecycle.MethodCallHandler: Handles messages received from Dart.
Here’s how to proceed:
- Initialise the MethodChannel: In the
onAttachedToEnginemethod, set up theMethodChannelusingflutterPluginBinding.binaryMessengerand the same channel name used in the Dart API. - Handle Method Calls: The
onMethodCallmethod will process incoming messages. Use theMethodCallobject to access the method name and arguments, and theResultobject to send responses back to Dart. - Retrieve Arguments Safely: Use
call.argument<T>("key")to extract parameters. Always complete the call by using one of theResultmethods:success(Object)to return a result.error(code, message, details)to report an error.notImplemented()if the method isn’t supported.
Before opening the Android project in Android Studio, run flutter build apk --config-only to generate the necessary build artefacts. Then, open the android folder in the plugin’s example directory to ensure Gradle dependencies resolve correctly.
iOS Implementation

The iOS implementation mirrors the Android approach but uses Objective-C or Swift:
- Set Up Native Code: Place your code in the
ios/Classesdirectory. UseFlutterMethodChannelwith the same channel name as in the Dart API to establish communication. - Handle Messages: Use
setMethodCallHandlerto listen for incoming calls. Process these calls in Swift or Objective-C and return results to Dart. If an error occurs, useFlutterErrorto send specific error codes and messages, which Dart will interpret as aPlatformException. - Register the Plugin: Use a static
register(with:)method to create the channel and set the handler. This method is called by theFlutterPluginRegistrar.
Keep in mind that iOS method channel handlers run on the UI thread. For tasks requiring significant processing, delegate them to background threads to maintain performance. For highly demanding operations, consider using Dart’s FFI (Foreign Function Interface) and mark Swift functions with @_cdecl("function_name") for direct access.
This setup ensures seamless communication between Dart and native platforms, enabling your plugin to perform its intended functionality.
Testing and Debugging Your Plugin
Once you’ve implemented the native functionality for your plugin, testing becomes crucial to ensure the Dart-to-native bridge works seamlessly across platforms. A solid testing process combines manual testing with automated tests that cover various aspects of your plugin’s architecture.
Manual Testing with the Example App
The example app generated with your plugin serves as a ready-made testing environment. To get started, navigate to the example folder and run:
flutter run This will launch the app on a connected Android or iOS device, allowing you to verify that platform channel calls are functioning as expected and delivering correct results.
Before diving into debugging, make sure to build the example app at least once with the following commands:
- For Android:
flutter build apk - For iOS:
flutter build ios
Building the app creates all the necessary platform-specific files and symlinks, ensuring smooth IDE integration. For Android debugging, open the example/android/build.gradle file in Android Studio by navigating to File > Open. For iOS, open example/ios/Runner.xcworkspace in Xcode. In Xcode, you’ll find your plugin’s native code under Pods/Development Pods in the Project Navigator. Both Android Studio and Xcode offer native debugging tools, such as breakpoints and profilers, to help you identify and fix platform-specific issues.
Once you’re satisfied with the manual testing, move on to automated testing to ensure more thorough reliability.
Automated Testing
Automated tests are essential for verifying that your plugin works as intended under different conditions. Among these, integration tests are particularly important because they check the end-to-end communication between Dart and native code on actual devices. Store these tests in the example/integration_test/ directory and run them using:
flutter test integration_test/app_test.dart As noted in Flutter’s documentation, “Dart integration tests… are often the most important tests for a plugin”. It’s a good idea to include at least one integration test for each platform channel call.
To complement integration tests, you can also write native unit tests to validate platform-specific logic in isolation. Here’s how:
- For Android: Place JUnit tests in
android/src/test/and run them with:./gradlew testDebugUnitTest(from the
example/androiddirectory). - For iOS: Add XCTest files to
example/ios/RunnerTests/and execute them using:xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug(from the
example/iosdirectory).
Native unit tests are particularly valuable when mocking system APIs that your plugin interacts with – something that’s not possible with Dart integration tests.
For Dart unit tests stored in the test/ directory, keep in mind that native code won’t load during execution. To avoid MissingPluginException errors, wrap platform channel calls with TestDefaultBinaryMessenger. If your plugin interacts with native UI elements, consider using native testing frameworks like Espresso for Android or XCUITest for iOS.
Publishing to pub.dev

Once you’ve thoroughly tested your plugin and confirmed it works as expected, the next step is to share it with the Flutter community by publishing it on pub.dev. Publishing ensures your plugin is permanently accessible to others. As highlighted in the official documentation:
Keep in mind that a published package lasts forever. As soon as you publish your package, users can depend on it.
This permanence means you cannot delete a published version, as doing so would disrupt projects that rely on your plugin.
Pre-Publishing Checklist
Before publishing, make sure your plugin meets all pub.dev requirements. Your package must include the following essential files:
pubspec.yaml: Contains metadata like the plugin’s name, version, description, and homepage URL.README.md: The main documentation that will appear on the package’s pub.dev page.CHANGELOG.md: Tracks the version history of your plugin.LICENSE: Specifies the licensing terms. The Dart and Flutter teams generally recommend using the BSD 3-clause licence.
Additionally, ensure your package remains under 100 MB after gzip compression and 256 MB uncompressed. To manage the size, use a .pubignore file to exclude unnecessary files like internal tools or large assets. Also, verify that your dependencies only include hosted packages from pub.dev or SDK dependencies such as sdk: flutter. Path or git dependencies are not allowed for published packages.
Run dart pub publish --dry-run (or flutter pub publish --dry-run) to simulate the publishing process. This command checks your package format, lists the files to be included, and flags any issues early. To ensure your code adheres to Flutter’s style guidelines, run dart analyze and dart format .. You can also generate API documentation locally using dart doc to verify that your documentation comments render correctly.
Once you’ve completed these steps, you’ll be ready to publish your plugin with confidence.
Publishing Your Plugin
To publish on pub.dev, you’ll need a Google Account. For added trust, consider creating a verified publisher account linked to a domain name, which provides a verified badge. If you initially publish using a personal account, you can later transfer ownership to a verified publisher through the Admin tab on your package’s pub.dev page.
From your plugin’s root directory, execute dart pub publish. Review the file list it generates, and if no errors are flagged, proceed with the upload. Follow semantic versioning (major.minor.patch) for your releases, and use suffixes like -dev.1 or -beta.2 for pre-releases.
If a critical issue arises, you have a seven-day window to retract a version. For future updates, you might want to streamline the process using GitHub Actions with OIDC tokens. This setup allows you to trigger new releases by pushing git tags like v1.0.0.
Conclusion
Creating a custom Flutter plugin is a step-by-step process that begins with project setup using flutter create --template=plugin. From there, you implement native code in Kotlin or Swift and eventually publish your work on pub.dev. Each stage is crucial for building a connection between Flutter’s Dart layer and the native platform, enabling access to hardware features, OS-specific APIs, or external libraries not included in Flutter’s core framework.
Once the plugin is functional, its real impact is felt when shared with the community. As Shahbaz Backer from Technaureus puts it:
By contributing to this ecosystem, you become a part of the global Flutter community.
Publishing your plugin not only promotes code reuse but also speeds up development for others, allowing them to focus on innovation rather than recreating existing solutions.
A federated plugin architecture is now the preferred approach for designing plugins. This method separates the app-facing API, platform interface, and platform-specific implementations into distinct packages. Chris Sells from Google’s Flutter team highlights the benefits of this structure:
Federated plugins… allow multiple teams with different expertise to bring their code together into a seamless experience for the Flutter developer using the plugin.
This design allows experts to add support for platforms like Windows, macOS, or Linux independently, without requiring the original developer to manage every platform.
Finally, meeting pub.dev standards ensures your plugin aligns with community expectations. High-quality plugins often receive high pub points and may even earn “Flutter Favourite” status, marking them as reliable tools within the ecosystem. By following these principles, your plugin not only achieves technical excellence but also contributes meaningfully to Flutter’s global community.
FAQs
When should I build a custom Flutter plugin?
When you require native platform functionality that isn’t offered by existing packages or plugins, it’s time to consider building a custom Flutter plugin. This approach is particularly useful for tapping into platform-specific features or implementing specialised logic that standard method channels or available solutions can’t handle.
Simply put, if no existing package aligns with your specific needs or platform requirements, creating a custom plugin is the way forward.
Should I use Method Channels or Dart FFI?
When deciding between Method Channels and Dart FFI, it all comes down to your specific requirements. Method Channels work best for most platform-specific tasks, providing a straightforward approach to call platform APIs and manage their responses. On the other hand, Dart FFI shines when you need direct, high-performance access to native libraries or C code. It’s particularly useful for tasks that require heavy computation, as it skips the overhead involved in message passing.
How do I avoid breaking changes when publishing to pub.dev?
To avoid causing breaking changes on pub.dev, it’s crucial to stick to proper versioning practices, such as semantic versioning. This involves updating the version number thoughtfully:
- Increase the major version for breaking changes.
- Bump the minor version for new features that don’t break existing functionality.
- Use the patch version for bug fixes or minor updates.
Make sure your pubspec.yaml file reflects these updates accurately. Additionally, take time to review package updates thoroughly before publishing. By managing versions responsibly, you help users update your package confidently, minimising the risk of unexpected problems.
