1. Introduction
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
- How to write a Flutter plugin for iOS and Android.
- How to create an API for your plugin.
- How to write an app that uses your plugin.
- How to publish your plugin so that others can use it.
What would you like to learn from this codelab?
2. Set up your Flutter environment
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 can run the codelab by using any of the following devices:
- A physical iOS or Android device connected to your computer and set to developer mode
- The iOS simulator (requires installing Xcode tools)
- The Android Emulator (requires setup in Android Studio)
3. Generate the plugin template
Flutter ships with templates for plugins that make 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 commands in your working directory to create the plugin template:
$ flutter create --template=plugin --org com.example --platforms=android,ios -a java -i objc plugin_codelab $ cd plugin_codelab $ cd example $ flutter pub upgrade $ dart migrate --apply-changes
These commands generate the following directory structure:
plugin_codelab ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android │ ├── build.gradle │ ├── gradle │ │ └── wrapper │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── local.properties │ ├── plugin_codelab_android.iml │ ├── settings.gradle │ └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── example │ └── plugin_codelab │ └── PluginCodelabPlugin.java ├── example │ ├── README.md │ ├── android │ ├── build │ │ └── ios │ │ └── Runner.build │ │ └── Release-iphoneos │ │ └── Runner.build │ │ └── dgph │ ├── ios │ ├── lib │ │ └── main.dart │ ├── plugin_codelab_example.iml │ ├── pubspec.lock │ ├── pubspec.yaml │ └── test │ └── widget_test.dart ├── ios │ ├── Assets │ ├── Classes │ │ ├── PluginCodelabPlugin.h │ │ └── PluginCodelabPlugin.m │ └── plugin_codelab.podspec ├── lib │ └── plugin_codelab.dart ├── plugin_codelab.iml ├── pubspec.lock ├── pubspec.yaml └── test └── plugin_codelab_test.dart
Here is a description of some important files:
pubspec.yaml
—The YAML file that defines your plugin. It specifies the plugin's name, dependencies, version, supported operating systems, and so on. This is used on your plugin's pub.dev page.CHANGELOG.md
—Any time that you want to publish a new version of a plugin you must update this markdown file to indicate the changes in the new version.README.md
—This markdown file shows up on the front page of the plugin's pub.dev listing. It should describe, in detail, what the plugin is and how to use it.lib/plugin_codelab.dart
—The Dart code that implements the frontend to your plugin. Plugin clients have access to the public classes and functions in this directory.android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java
—The native Java code that implements the Android feature described inplugin_codelab.dart
.ios/Classes/PluginCodelabPlugin.m
—The Objective-C code that implements the iOS feature described inplugin_codelab.dart.
(There is a matching header file as well.)example/
—This directory contains a client of your plugin. While developing your plugin, you edit this file to see your plugin in action.example/lib/main.dart
—The Dart code that exercises your plugin. You build the example UI here.
4. Build and run the example
Run the examples on your iOS or Android device with the following instructions:
$ cd plugin_codelab/example $ flutter run
You should see something like this:
Check out the generated code for the plugin's frontend:
lib/plugin_codelab.dart
class PluginCodelab {
static const MethodChannel _channel =
const MethodChannel('plugin_codelab');
static Future<String> get platformVersion async {
final String version =
await _channel.invokeMethod('getPlatformVersion');
return version;
}
}
Observations
PluginCodelab
is the class that users of your plugin invoke.- This class creates a
MethodChannel
that allows the Dart code to communicate with the host platform. - The plugin's API has only one method, the property getter
platformVersion
. When someone calls this getter in Dart, theMethodChannel
invokes thegetPlatformVersion()
method and asynchronously waits for aString
to be returned. - It's up to the platform-specific code to interpret the meaning of the
getPlatformVersion
message, and you'll see that later.
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
- This is a client of the plugin.
- This code calls the getter defined in
lib/plugin_codelab.dart
. - Notice that the call is wrapped in a
try-block
. If the platform-specific code for iOS returns a FlutterError, or an exception is thrown in Java, then it gets resurfaced on the Dart side.
ios/Classes/PluginCodelabPlugin.m
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"plugin_codelab"
binaryMessenger:[registrar messenger]];
PluginCodelabPlugin* instance = [[PluginCodelabPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
Observations
- This initialization code is called when a new engine is set up.
- This code generates a channel for communicating with the plugin.
- Notice that the channel name specified here must match the name defined in
lib/plugin_codelab.dart
. - Setting itself up as the
methodCallDelegate
means that the created instance receives messages that are associated with the provided binary messenger.
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
- This Objective-C code implements the
getPlatformVersion()
method for iOS. - The
result
callback is invoked with anNSString
parameter, which is automatically converted by the method channel's codec.
android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "plugin_codelab");
channel.setMethodCallHandler(this);
}
Observations
- This Java code implements
getPlatformVersion()
for Android.
android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java
@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
- This Java code handles messages sent from Dart. Notice that this code is similar in form to the iOS plugin, but has some subtle differences.
5. Add the platform-specific code
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/plugin_codelab.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/plugin_codelab/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.
6. Design the plugin API
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/plugin_codelab.dart
import 'dart:async';
import 'package:flutter/services.dart';
class PluginCodelab {
static const MethodChannel _channel = const MethodChannel('plugin_codelab');
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.
7. Implement the plugin platform code
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/plugin_codelab/PluginCodelabPlugin.java
public class PluginCodelabPlugin implements FlutterPlugin, MethodCallHandler {
private MethodChannel channel;
private Synth synth;
private static final String channelName = "plugin_codelab";
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.
8. Implement the example UI
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:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:plugin_codelab/plugin_codelab.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:
- You must import
'package:plugin_codelab/plugin_codelab.dart'
in order to use the plugin. The dependency of the example on the plugin is defined inexample/pubspec.yaml,
which makes this work. - In
main(),
the orientation is forced to be landscape so that the whole keyboard can fit on the screen. - The
_onKeyDown()
and_onKeyUp()
methods are both clients of the plugin API designed in previous steps. - The code uses
InkWell
, which are just interactive rectangles, to draw the individual keys.
Run the app to see your functioning music keyboard:
cd example flutter run
It should look like this:
9. Congratulations!
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/plugin_codelab for comparison.
Next steps
- Add end-to-end testing. The Flutter team provides a library to create end-to-end integration tests called e2e.
- Publish to pub.dev. After you create a plugin, you may want to share it online so that others can use it. You can find the full documentation on publishing your plugin to pub.dev in Developing plugin packages.
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:
- Right now the synthesizer generates a sine wave. How about generating a saw wave?
- Did you notice the popping sounds when you press and release a key? That's due to the oscillator abruptly turning on and off. Usually synthesizers remedy that with amplitude envelopes.
- Right now you can only play one key at a time. That's called monophonic. Real pianos are polyphonic.