Article

Flutter Plugin Development: A Comprehensive Guide: Part 1

August 6, 2024

By Derrik Fleming | Senior Software Engineer |

Introduction

Welcome to the world of Flutter plugin development! If you’re a technical leader or a software developer looking to expand your skill set, mastering the development of Flutter plugins is a fantastic way to enhance your apps’ capabilities. In this multi-part guide, we’ll walk you through the essentials of creating and integrating custom Flutter plugins, from the basics to advanced technical details and best practices.

Part 1 of this series will focus mainly on plugin specific setup and developing a general understanding of Flutter plugins, and how you can use them to develop interfaces to native APIs for Flutter application developers to utilize.

Basics and Setup

Flutter plugins are essential tools that enable you to extend the functionality of your Flutter apps by integrating platform-specific code. Whether you want to access device features like cameras, sensors, or even complex APIs, plugins act as the bridge between Flutter’s Dart code and the underlying native code (Android, iOS, web, etc.). 

Mastering Flutter plugin development can save you significant time and effort, making your development process more efficient and your apps more powerful.

Prerequisites

As a prerequisite to the remainder of this article you should have your development environment configured and prepared virtual or physical mobile devices for development purposes. The Flutter documentation is a stellar source of information, if you haven’t yet gone through the required Getting Started steps, I recommend starting here.

Flutter plugin development setup

Over the course of developing a number of Flutter plugins at SpinDance we have come across a few tips/tricks and tools along the way that may prove to enhance your experience and the overall quality of Flutter plugins you might develop.

The first is a tool targeted at Dart and Flutter developers called very_good_cli. It is a command-line interface that provides a set of opinionated templates as scaffolding for a number of different types of Dart and Flutter development, one of those being Flutter plugins. Along with the template you’ll get Dart analysis options that build on the Flutter team’s own recommendations and some other helpful tools.

Installing very_good_cli

To install very_good_cli run the following command:

dart pub global activate very_good_cli

Or see the docs for other installation options, here.

Creating a Federated Flutter Plugin

Creating your first Flutter plugin can be a rewarding learning experience. If you don’t have practical experience in one or more of the platforms that you’re looking to support, you can use it as an opportunity to dive into the best practices and nuances of developing native code on any or all of the platforms Flutter supports. 

We’ll be using very_good_cli to create the basis of our plugin because, unlike Flutter’s CLI, it supports the Federated Plugin Architecture by default. If you’re interested in the template you can find it here. You are not required to use this architecture but it does create a very clean separation between the plugin’s platform interface, platform specific implementations of it, related native code, and allows you and/or other contributors to add new platforms at a later point in time without having to touch code related to platforms already supported. You can read more about this plugin architecture here.

Using very_good_cli create a Flutter plugin for Android and iOS with the command below:

very_good create flutter_plugin my_flutter_plugin --desc "My new Flutter plugin" --platforms android,ios

Technical Deep Dive

Plugin Architecture Overview

Once the plugin has been created navigate to the top-level directory, and let’s take a look around. You should see a directory structure as pictured below.

A federated Flutter plugin is a plugin composed of multiple packages. Each one of these top-level directories is a separate package, noted by the pubspec.yaml you’ll find in each one.

Let’s go over the generals of what the previous very_good create command gave us with its Hello, World!-type example to illustrate with more color how these packages are intended to be used. 

my_flutter_plugin_platform_interface

A package where the my_flutter_plugin’s abstract extension of the PlatformInterface will live. It describes the interface that the plugin will use to interact with various platforms to provide the desired functionality to developers using the plugin. In very_good’s implementation you can see they’ve specified a method getPlatformName() to be implemented in concrete implementations.

import 'package:my_flutter_plugin_platform_interface/src/method_channel_my_flutter_plugin.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

abstract class MyFlutterPluginPlatform extends PlatformInterface {
  MyFlutterPluginPlatform() : super(token: _token);

  static final Object _token = Object();

  static MyFlutterPluginPlatform _instance = MethodChannelMyFlutterPlugin();

  static MyFlutterPluginPlatform get instance => _instance;

  static set instance(MyFlutterPluginPlatform instance) {
    PlatformInterface.verify(instance, _token);
    _instance = instance;
  }

  Future<String?> getPlatformName();
}
my_flutter_plugin_android & my_flutter_plugin_ios

As you might guess based on the name, these packages will contain platform-specific extensions of the class defined in the PlatformInterface package, as well as any native code to handle PlatformChannels. Below you will find the Android and iOS extensions MyFlutterPluginPlatform.

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:my_flutter_plugin_platform_interface/my_flutter_plugin_platform_interface.dart';

/// The Android implementation of [MyFlutterPluginPlatform].
class MyFlutterPluginAndroid extends MyFlutterPluginPlatform {
  @visibleForTesting
  final methodChannel = const MethodChannel('my_flutter_plugin_android');

  static void registerWith() {
    MyFlutterPluginPlatform.instance = MyFlutterPluginAndroid();
  }

  @override
  Future<String?> getPlatformName() {
    return methodChannel.invokeMethod<String>('getPlatformName');
  }
}
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:my_flutter_plugin_platform_interface/my_flutter_plugin_platform_interface.dart';

/// The iOS implementation of [MyFlutterPluginPlatform].
class MyFlutterPluginIOS extends MyFlutterPluginPlatform {
  @visibleForTesting
  final methodChannel = const MethodChannel('my_flutter_plugin_ios');

  static void registerWith() {
    MyFlutterPluginPlatform.instance = MyFlutterPluginIOS();
  }

  @override
  Future<String?> getPlatformName() {
    return methodChannel.invokeMethod<String>('getPlatformName');
  }
}
my_flutter_plugin

my_flutter_plugin is where non-platform-specific details can be implemented, anything that developers might need to use your plugin effectively in the context of a Flutter application, and of course a set of functions that call methods on the PlatformInterface.

import 'package:my_flutter_plugin_platform_interface/my_flutter_plugin_platform_interface.dart';

MyFlutterPluginPlatform get _platform => MyFlutterPluginPlatform.instance;

/// Returns the name of the current platform.
Future<String> getPlatformName() async {
  final platformName = await _platform.getPlatformName();

  if (platformName == null) throw Exception('Unable to get platform name.');

  return platformName;
}


If you’re looking for a good set of references of real-world federated Flutter plugins Google’s Flutter team maintains a slew of them. You can check out all of them here, but one in particular that I’ve found myself gravitating to for reference is url_launcher.

You might also be interested in flutter-esp-provisioning, a Flutter plugin that enables Flutter application developers to leverage Espressif’s native provisioning libraries. SpinDance developed the plugin for use in our Callbox mobile SDK for Flutter.

Platform Channel Communication

PlatformChannel’s enable communication between Dart and native code. They are essential for integrating platform-specific functionality into your Flutter plugins. Let’s go over the different types of channels that Flutter provides for enabling communication between Dart and native code. 

MethodChannels

For straightforward method calls, use MethodChannel. It’s suitable for most use cases where you need to send and receive data between Dart and native code. MethodChannels are ideal when you have discrete operations, such as retrieving a value, invoking a function, or setting a property. They operate synchronously, meaning the Dart side will wait for the native side to complete the operation and return the result. This is particularly useful for tasks that are quick and need immediate responses, such as getting battery level or sending user inputs.

EventChannels

If you need continuous data streams (e.g., sensor data), use EventChannels. EventChannels are designed for scenarios where you need to send a stream of data from the native code to the Dart code. This is beneficial for real-time data updates or monitoring changes, such as receiving accelerometer readings, listening to network status changes, or tracking location updates. EventChannels provide an asynchronous communication mechanism, which ensures that data can be sent continuously and processed efficiently on the Dart side.

Working with PlatformChannels
Error handling

Ensure that any errors in the platform code are caught and communicated back to the Dart side. This helps in debugging and provides a better user experience. Error handling is crucial because it allows you to provide meaningful feedback to the user and developers. On the native side, always catch exceptions and send error messages back to the Dart side using the appropriate channel methods. On the Dart side, handle these errors gracefully by displaying user-friendly messages, logging the errors for further investigation, and implementing fallback mechanisms if necessary. This approach not only improves the reliability of your plugin but also enhances the overall user experience by preventing crashes and unexpected behaviors.

Data De/Serialization

When working with PlatformChannels, you’ll often need to pass complex objects between Dart and native code. Efficient serialization is key to ensuring smooth communication. There are a number of options to consider when approaching serialization in Dart.

Serialization Options

The following list includes some of the options you have available to you in Dart for serialization

  • Manual
    While more labor-intensive, manual serialization offers complete control over how objects are serialized and deserialized. This method can be useful for simple data structures or when you need to optimize performance for specific scenarios.
  • protobuf
    Protocol Buffers, or Protobuf, is a method developed by Google for serializing structured data. It is more compact and efficient than JSON, making it ideal for performance-critical applications. Protobuf requires defining a schema for your data structures, which can then be compiled into Dart code for serialization and deserialization. Check out the Dart package here.
  • freezed
    The freezed package is a code generator for immutable classes. It provides built-in support for JSON serialization and can work seamlessly with the json_serializable package. Using freezed helps in maintaining immutability and ensures data consistency throughout your application. Check it out here.
  • json_serializable
    This Dart package automates the process of converting Dart objects to and from JSON. It generates serialization code based on annotations, reducing boilerplate and ensuring consistency. JSON is a widely used format, making it easy to debug and integrate with various systems. Check it out here.

Wrapping up

Flutter plugin development opens up a world of possibilities for extending the functionality of your Flutter apps by integrating platform-specific features. With the foundational knowledge from this guide, you should now have a solid grasp of how to set up your development environment, create and manage federated plugins, and communicate efficiently between Dart and native code. As you continue to develop your skills, remember that practice and experimentation are key. Don’t hesitate to explore further, experiment with different approaches, and seek out additional resources. The Flutter community is vibrant and full of shared knowledge, so you’re never alone on this journey.

If you have any questions or need further assistance, feel free to reach out. Your adventure in Flutter plugin development is just beginning, and there’s a lot more to discover and build. Check back in at a later date for the second part of this series where we’ll build on the foundation that we’ve laid today with the help of very_good_cli

Happy hacking!