Welcome to the "How to write a Flutter plugin" codelab!

Just what is a plugin?

A plugin is a piece of software that adds capabilities to your app. For example, you might want your mobile app to interact with the camera on your device. Plugins are an important part of the Flutter ecosystem. You should first check pub.dev to see if the plugin you need already exists. The authors of the Flutter SDK, as well as members of the Flutter community, have written many plugins and published them to pub.dev to share them with the community.

In particular, you should check out the Flutter Favorite packages and plugins. The Flutter Favorite Favorites tag identifies plugins that you should first consider when building your apps.

Flutter makes it easy for users to interact with cross-platform Dart libraries, but sometimes it's ideal to interact with platform-specific code. For example, you might want to communicate with a database that doesn't have a Dart library written for it. Flutter provides a mechanism for authoring plugins that allows you to communicate with platform-specific code and also allows you to publish your plugins on pub.dev so that others can use them.

In this codelab, you'll learn how to author your own plugins for iOS and Android. You'll implement a simple music plugin that processes audio on the host platform, and then you'll make an example app that uses your plugin to make a music keyboard.

Here are screenshots of the final app:

What you learn

What would you like to learn from this codelab?

I'm new to the topic, and I want a good overview. I know something about this topic, but I want a refresher. I'm looking for example code to use in my project. I'm looking for an explanation of something specific.

You need two pieces of software to complete this lab: the Flutter SDK and an editor. You can use your preferred editor, such as Android Studio or IntelliJ with the Flutter and Dart plugins installed, or Visual Studio Code with the Dart Code and Flutter extensions.

Some of the tooling for plugin development recently changed, so this codelab assumes v1.15.19 or later of the Flutter SDK. You can check your version with the following command:

$ flutter doctor

You may have to change to the beta channel of Flutter to have access to that version. You can do that with the following command:

$ flutter channel beta

You can run the codelab by using any of the following devices:

Flutter ships with templates for plugins that makes it easy to get started. When you generate the plugin template, you can specify which language you want to use. The default is Swift for iOS and Kotlin for Android. For this codelab, you use Objective-C and Java.

Run the following command in your working directory to create the plugin template:

$ flutter create --template=plugin --ios-language=objc --android-language=java PluginCodelab

This command generates the following directory structure:

PluginCodelab/
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android/
│   ├── build.gradle
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           └── java/
│               └── com/
│                   └── example/
│                       └── PluginCodelab/
│                           └── PluginCodelabPlugin.java
├── example/
│   ├── README.md
│   ├── android/
│   ├── ios/
│   ├── lib/
│   │   └── main.dart
│   ├── pubspec.yaml
│   ├── test/
│   │   └── widget_test.dart
├── ios/
│   ├── PluginCodelab.podspec
│   ├── Assets/
│   └── Classes/
│       ├── PluginCodelabPlugin.h
│       └── PluginCodelabPlugin.m
├── lib/
│   └── PluginCodelab.dart
├── pubspec.yaml
└── test/
    └── PluginCodelab_test.dart

Here is a description of some important files:

Run the examples on your iOS or Android device with the following instructions:

$ cd PluginCodelab/example
$ flutter run

You should see something like this:

Check out the generated code for the plugin's frontend:

lib/PluginCodelab.dart

class PluginCodelab {
  static const MethodChannel _channel =
      const MethodChannel('PluginCodelab');

  static Future<String> get platformVersion async {
    final String version =
      await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

Observations

example/lib/main.dart

Future<void> initPlatformState() async {
  String platformVersion;
  // Platform messages may fail, so we use a try/catch PlatformException.
  try {
    platformVersion = await PluginCodelab.platformVersion;
  } on PlatformException {
    platformVersion = 'Failed to get platform version.';
  }

  // If the widget was removed from the tree while the asynchronous platform
  // message was in flight, we want to discard the reply rather than calling
  // setState to update our non-existent appearance.
  if (!mounted) return;

  setState(() {
    _platformVersion = platformVersion;
  });
}

Observations

ios/Classes/PluginCodelabPlugin.m

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  FlutterMethodChannel* channel = [FlutterMethodChannel
      methodChannelWithName:@"PluginCodelab"
            binaryMessenger:[registrar messenger]];
  PluginCodelabPlugin* instance = [[PluginCodelabPlugin alloc] init];
  [registrar addMethodCallDelegate:instance channel:channel];
}

Observations

ios/Classes/PluginCodelabPlugin.m

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else {
    result(FlutterMethodNotImplemented);
  }
}

Observations

android/src/main/java/com/example/PluginCodelab/PluginCodelabPlugin.java

@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
  channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "PluginCodelab");
  channel.setMethodCallHandler(this);
}

Observations

ios/Classes/PluginCodelabPlugin.m

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  if (call.method.equals("getPlatformVersion")) {
    result.success("Android " + android.os.Build.VERSION.RELEASE);
  } else {
    result.notImplemented();
  }
}

Observations


You've found something special!

Now, you provide the platform-specific implementations for a synthesizer that makes sounds when pressing keys on the keyboard. You can think of this code as the library you will surface to Flutter. Often, when making a plugin, you already have a defined platform API that you'll work from, as in this case.

You now have two separate implementations of the same functionality, one for iOS and one for Android. You need to get these compiling as part of your app so that the plugin can call into it.

Add to iOS

Add the following files to your project:

By placing these files in the ios/Classes location, they will compile as part of the iOS build for your plugin. You can look at ios/PluginCodelab.podspec to see that, by default, it uses globs to define which sources to compile. All you need to do is put the files in the right location.

Add to Android

Add the following file to your project:

android/src/main/java/com/example/PluginCodelab/Synth.java

By placing this Java file in the android/src/main/java/com/example location, it will compile as part of the Android build for your plugin. You can look at the Gradle build system to see that you only need to place the file in the correct directory to get it to compile.

Synthesizer interface explanation

The synthesizer interface is similar on iOS and Android, and consists of four methods:

class Synthesizer {
  void start();
  void stop();
  int keyDown(int key);
  int keyUp(int key);
}

The keyUp() and keyDown() methods represent the events sent when a key on the musical keyboard is pressed down and released. The key argument represents which key is being pressed or released. It's an enumeration of all the keys on the musical keyboard. The MIDI standard defines an enumeration for those keys, where 60 is the value for Middle C and increments one for every black or white key (semitone). Your plugin uses this definition.

The next step in making a plugin is thinking about what sort of information you want to send back and forth between Flutter and the host platform. If you're trying to represent a library that already has an API defined, you can make your life easy, and mimic that interface.

In this codelab, we provide the synthesizer code for each platform, so you can mimic its interface in the Dart code:

lib/PluginCodelab.dart

import 'dart:async';

import 'package:flutter/services.dart';

class PluginCodelab {
  static const MethodChannel _channel =
      const MethodChannel('PluginCodelab');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }

  static Future<int> onKeyDown(int key) async {
    final int numNotesOn = await _channel.invokeMethod('onKeyDown', [key]);
    return numNotesOn;
  }

  static Future<int> onKeyUp(int key) async {
    final int numNotesOn = await _channel.invokeMethod('onKeyUp', [key]);
    return numNotesOn;
  }
}

Notice that the second parameter to invokeMethod() lists the parameters that are sent to the method call.

Now you have platform-specific libraries for making sound and Dart code that controls that code, but they aren't hooked up. If you call any of these Dart methods now, they result in "Not Implemented" exceptions because you haven't implemented the host side in the plugin. That's the next step.

Hooking things up on iOS

First, modify the plugin to create and start a synthesizer instance:

ios/Classes/PluginCodelabPlugin.m

@implementation PluginCodelabPlugin {
  int _numKeysDown;
  FLRSynthRef _synth;
}
- (instancetype)init {
  self = [super init];
  if (self) {
    _synth = FLRSynthCreate();
    FLRSynthStart(_synth);
  }
  return self;
}

- (void)dealloc {
  FLRSynthDestroy(_synth);
}

Next, start handling messages sent over the channel:

- (void)handleMethodCall:(FlutterMethodCall *)call
                  result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS "
        stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else if ([@"onKeyDown" isEqualToString:call.method]) {
    FLRSynthKeyDown(_synth, [call.arguments[0] intValue]);
    _numKeysDown += 1;
    result(@(_numKeysDown));
  } else if ([@"onKeyUp" isEqualToString:call.method]) {
    FLRSynthKeyUp(_synth, [call.arguments[0] intValue]);
    
    _numKeysDown -= 1;
    result(@(_numKeysDown));
  } else {
    result(FlutterMethodNotImplemented);
  }
}

Notice that the code now looks for the onKeyDown and onKeyUp messages as well. In order to get the key argument, pull it from call.arguments. The returned value is boxed as an NSNumber (described in the Platform channels documentation), so convert it with intValue.

See the completed file, PluginCodelabPlugin.m.

Hooking things up on Android

First, modify the plugin to create and start a synthesizer instance:

android/src/main/java/com/example/PluginCodelab/PluginCodelabPlugin.java

public class PluginCodelabPlugin implements FlutterPlugin, MethodCallHandler {
  private MethodChannel channel;
  private Synth synth;
  private static final String channelName = "PluginCodelab";

  private static void setup(PluginCodelabPlugin plugin, BinaryMessenger binaryMessenger) {
    plugin.channel = new MethodChannel(binaryMessenger, channelName);
    plugin.channel.setMethodCallHandler(plugin);
    plugin.synth = new Synth();
    plugin.synth.start();
  }

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    setup(this, flutterPluginBinding.getBinaryMessenger());
  }

Next, start handling messages sent over the channel:

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  if (call.method.equals("getPlatformVersion")) {
    result.success("Android " + android.os.Build.VERSION.RELEASE);
  } else if (call.method.equals("onKeyDown")) {
    try {
      ArrayList arguments = (ArrayList) call.arguments;
      int numKeysDown = synth.keyDown((Integer) arguments.get(0));
      result.success(numKeysDown);
    } catch (Exception ex) {
      result.error("1", ex.getMessage(), ex.getStackTrace());
    }
  } else if (call.method.equals("onKeyUp")) {
    try {
      ArrayList arguments = (ArrayList) call.arguments;
      int numKeysDown = synth.keyUp((Integer) arguments.get(0));
      result.success(numKeysDown);
    } catch (Exception ex) {
      result.error("1", ex.getMessage(), ex.getStackTrace());
    }
  } else {
    result.notImplemented();
  }
}

Similar to iOS, the code now looks for the onKeyDown and onKeyUp messages. Use arguments.get() to extract the key value here, as well. Make sure that, on Android, your plugin handles any exceptions that might arise.

See the completed file, PluginCodelabPlugin.java.

Now that the plugin implements all of the plumbing, you probably want to see it in action. For that, you implement a simple keyboard UI example app:

example/lib/main.dart

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:PluginCodelab/PluginCodelab.dart';

enum _KeyType { Black, White }

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight])
      .then((_) {
    runApp(new MyApp());
  });
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String platformVersion;
    try {
      platformVersion = await PluginCodelab.platformVersion;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  void _onKeyDown(int key) {
    print("key down:$key");
    PluginCodelab.onKeyDown(key).then((value) => print(value));
  }

  void _onKeyUp(int key) {
    print("key up:$key");
    PluginCodelab.onKeyUp(key).then((value) => print(value));
  }

  Widget _makeKey({@required _KeyType keyType, @required int key}) {
    return AnimatedContainer(
      height: 200,
      width: 44,
      duration: Duration(seconds: 2),
      curve: Curves.easeIn,
      child: Material(
        color: keyType == _KeyType.White
            ? Colors.white
            : Color.fromARGB(255, 60, 60, 80),
        child: InkWell(
          onTap: () => _onKeyUp(key),
          onTapDown: (details) => _onKeyDown(key),
          onTapCancel: () => _onKeyUp(key),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Color.fromARGB(255, 250, 30, 0),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              Text('Running on: $_platformVersion\n'),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  _makeKey(keyType: _KeyType.White, key: 60),
                  _makeKey(keyType: _KeyType.Black, key: 61),
                  _makeKey(keyType: _KeyType.White, key: 62),
                  _makeKey(keyType: _KeyType.Black, key: 63),
                  _makeKey(keyType: _KeyType.White, key: 64),
                  _makeKey(keyType: _KeyType.White, key: 65),
                  _makeKey(keyType: _KeyType.Black, key: 66),
                  _makeKey(keyType: _KeyType.White, key: 67),
                  _makeKey(keyType: _KeyType.Black, key: 68),
                  _makeKey(keyType: _KeyType.White, key: 69),
                  _makeKey(keyType: _KeyType.Black, key: 70),
                  _makeKey(keyType: _KeyType.White, key: 71),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

Notice the following:

Run the app to see your functioning music keyboard:

cd example
flutter run

It should look like this:

Congratulations! You successfully created a Flutter plugin for iOS and Android, and you have a nifty musical keyboard to jam on. The completed project can be downloaded from https://github.com/flutter/codelabs/tree/master/PluginCodelab for comparison.

Next steps

Extend the synthesizer

For fun, if you want to play around with the synthesizer and improve it, here are some next steps you might consider: