Flutter, Google’s UI toolkit, allows developers to build natively compiled applications for mobile, web, and desktop from a single codebase. One of Flutter’s standout features is its ability to interact with platform-specific code via plugins. Custom Flutter plugins enable you to encapsulate reusable native functionality, granting your Flutter app access to features not available through Dart alone. This article will guide you through the process of creating custom Flutter plugins to integrate native functionality seamlessly.
What are Flutter Plugins?
Flutter plugins are packages that allow Flutter code to interact with platform-specific APIs, native libraries, or third-party SDKs. These plugins consist of:
- Dart Code: Defines the API used by your Flutter app.
- Platform-Specific Code (Android, iOS, Web, etc.): Implements the native functionality on each supported platform.
- Platform Interface: Establishes communication between Dart and native code using method channels or Pigeon.
Why Create Custom Flutter Plugins?
Creating custom Flutter plugins is beneficial for several reasons:
- Access Native Features: Use device hardware features, platform-specific services, or APIs unavailable in Flutter/Dart.
- Code Reusability: Encapsulate reusable native functionality in a modular package.
- Performance Optimization: Execute performance-critical tasks in native code.
- Cross-Platform Support: Maintain a consistent Dart API while leveraging native capabilities across different platforms.
Steps to Create a Custom Flutter Plugin
Step 1: Set Up the Plugin Project
Use the Flutter CLI to create a new plugin project:
flutter create --template=plugin my_custom_plugin
cd my_custom_plugin
This command generates a directory structure that includes:
lib/my_custom_plugin.dart: Dart API for your plugin.android/: Android-specific code.ios/: iOS-specific code.example/: A Flutter app to test the plugin.
Step 2: Define the Dart API
Open lib/my_custom_plugin.dart and define the public API for your plugin. This involves:
- Importing
flutter/services.dart: Necessary for usingMethodChannelto communicate with native code. - Creating a Class: Defining the main plugin class.
- Defining Methods: Creating asynchronous methods that invoke corresponding native methods.
import 'dart:async';
import 'package:flutter/services.dart';
class MyCustomPlugin {
static const MethodChannel _channel =
const MethodChannel('my_custom_plugin');
static Future getPlatformVersion() async {
final String? version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
static Future add(int a, int b) async {
final int? sum = await _channel.invokeMethod('add', {'a': a, 'b': b});
return sum;
}
}
Here’s what the code does:
- A
MethodChannelnamed_channelis created to handle communication. - The
getPlatformVersionmethod invokes a native method with the same name. - The
addmethod invokes a native method named ‘add’, sending the parameters a and b as arguments in a map.
Step 3: Implement Platform-Specific Code (Android)
Navigate to android/src/main/kotlin/com/example/my_custom_plugin/MyCustomPlugin.kt and implement the Android-specific functionality.
package com.example.my_custom_plugin
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
class MyCustomPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "my_custom_plugin")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
}
"add" -> {
val a = call.argument("a") ?: 0
val b = call.argument("b") ?: 0
result.success(a + b)
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
In this Kotlin code:
- The plugin class implements
FlutterPluginandMethodCallHandler. onAttachedToEngineinitializes theMethodChannel.onMethodCallhandles method calls from Dart:getPlatformVersionreturns the Android version.addretrieves two arguments, adds them, and returns the result.- If a method is not implemented,
result.notImplemented()is called. onDetachedFromEnginenullifies theMethodChannel.
Step 4: Implement Platform-Specific Code (iOS)
Navigate to ios/Classes/MyCustomPlugin.m (or MyCustomPlugin.swift if using Swift) and implement the iOS-specific functionality.
Objective-C:
#import "MyCustomPlugin.h"
#import
@implementation MyCustomPlugin
+ (void)registerWithRegistrar:(NSObject*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"my_custom_plugin"
binaryMessenger:[registrar messenger]];
MyCustomPlugin* instance = [[MyCustomPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else if ([@"add" isEqualToString:call.method]) {
NSNumber *a = call.arguments[@"a"];
NSNumber *b = call.arguments[@"b"];
if (a != nil && b != nil) {
result(@([a intValue] + [b intValue]));
} else {
result([FlutterError errorWithCode:@"INVALID_ARGUMENT"
message:@"Missing argument"
details:nil]);
}
} else {
result(FlutterMethodNotImplemented);
}
}
@end
Swift:
import Flutter
import UIKit
public class MyCustomPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "my_custom_plugin", binaryMessenger: registrar.messenger())
let instance = MyCustomPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
case "add":
guard let args = call.arguments as? [String: Any],
let a = args["a"] as? Int,
let b = args["b"] as? Int else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing arguments", details: nil))
return
}
result(a + b)
default:
result(FlutterMethodNotImplemented)
}
}
}
In both the Objective-C and Swift implementations:
registerWithRegistrar/registerregisters the plugin with Flutter.handleMethodCall/handlehandles method calls from Dart:getPlatformVersionreturns the iOS version.addretrieves two arguments, adds them, and returns the result.- If a method is not implemented,
result(FlutterMethodNotImplemented)is called.
Step 5: Test the Plugin
In the example directory of your plugin project, modify the main.dart file to test the plugin.
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:my_custom_plugin/my_custom_plugin.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
String _platformVersion = 'Unknown';
int _sumResult = 0;
@override
void initState() {
super.initState();
initPlatformState();
}
Future initPlatformState() async {
String? platformVersion;
try {
platformVersion = await MyCustomPlugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
int? sum;
try {
sum = await MyCustomPlugin.add(5, 3) ?? 0;
} on PlatformException {
sum = 0;
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
_sumResult = sum!;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Running on: $_platformVersionn'),
Text('5 + 3 = $_sumResultn'),
],
),
),
),
);
}
}
Here, main.dart calls getPlatformVersion and add methods, displaying the results in the UI.
Step 6: Run the Example App
Navigate to the example directory and run the Flutter app:
cd example
flutter run
Test the plugin on both Android and iOS simulators or physical devices to ensure correct behavior across platforms.
Advanced Considerations
Using Pigeon for Communication
For more complex plugins, consider using Pigeon to generate type-safe communication code. Pigeon simplifies the process of defining the communication interface and generating code for both Dart and native platforms. Define a Pigeon file, generate the code, and use the generated methods to interact with your native functionality.
// pigeon.dart
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/src/messages.g.dart',
kotlinOut:
'android/src/main/kotlin/com/example/my_custom_plugin/Messages.g.kt',
swiftOut: 'ios/Classes/Messages.g.swift',
),
)
class Input {
int a;
int b;
}
class Result {
int result;
}
@HostApi()
abstract class Api {
Result add(Input input);
}
Handling Platform Events
For real-time or event-driven interactions, use EventChannel to stream data from native code to Flutter. Create an EventChannel in your Dart code, set up a stream handler in your native code, and send events to the stream.
Packaging and Publishing the Plugin
When your plugin is ready for distribution:
- Update
pubspec.yaml: Include necessary information about your plugin. - Document Your Plugin: Provide clear and comprehensive documentation.
- Publish to
pub.dev: Runflutter pub publishto make your plugin available to the Flutter community.
Conclusion
Creating custom Flutter plugins allows developers to leverage the power of native functionalities, enriching Flutter applications beyond the Dart environment. By setting up a plugin project, defining a Dart API, implementing platform-specific code, and thoroughly testing, developers can ensure seamless and efficient native integrations. The steps outlined in this guide provide a comprehensive roadmap for creating, testing, and publishing custom Flutter plugins to extend Flutter’s capabilities. The ability to tap into native resources optimizes performance and extends the functionalities of Flutter applications, paving the way for more versatile and capable apps across various platforms.