Flutter’s popularity stems from its ability to create beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. However, there are times when you need to access platform-specific features or functionalities that are not available through Flutter’s built-in widgets and APIs. This is where custom Flutter plugins come in.
What are Flutter Plugins?
Flutter plugins are packages that allow Flutter code to interact with native platform code (e.g., Swift/Objective-C for iOS and Kotlin/Java for Android). They act as a bridge, enabling Flutter apps to use platform-specific features and functionalities that are not part of the core Flutter framework.
Why Build Custom Flutter Plugins?
- Access native APIs (e.g., Bluetooth, GPS, sensors).
- Utilize existing native libraries.
- Performance optimization by delegating tasks to native code.
- Platform-specific customizations.
Steps to Build a Custom Flutter Plugin
Here’s a comprehensive guide on how to create a custom Flutter plugin, covering both Android and iOS implementations.
Step 1: Set Up the Plugin Project
First, create a new Flutter plugin project using the Flutter CLI:
flutter create --template=plugin my_custom_plugin
Replace my_custom_plugin
with the desired name for your plugin. This command sets up the basic directory structure:
my_custom_plugin/
├── android/
├── ios/
├── lib/
├── example/
├── ...
Step 2: Define the Plugin Interface (Dart)
Open lib/my_custom_plugin.dart
(or the equivalent based on your plugin name) and define the plugin’s API. This is the Dart interface that Flutter code will use to interact with your plugin.
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 nativeFunction(String argument) async {
final String? result = await _channel.invokeMethod('nativeFunction', {'argument': argument});
return result;
}
}
Key components:
MethodChannel
: Used to communicate with native code. The channel name (my_custom_plugin
) must be consistent across all platforms.getPlatformVersion
: An example method that calls a native function to retrieve the platform version.nativeFunction
: An example method that sends data(argument) to native function
Step 3: Implement the Android Platform Code (Kotlin/Java)
Navigate to the android/src/main/kotlin/
directory within your plugin project.
Edit the auto-generated Kotlin file (or create a Java file if you prefer) to 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
/** MyCustomPlugin */
class MyCustomPlugin: FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel : MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
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}")
}
"nativeFunction" -> {
val argument = call.argument("argument")
if (argument != null) {
//Native Code implementation
result.success("Android Native Result: $argument")
} else {
result.error("ARGUMENT_ERROR", "Argument is null", null)
}
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
Explanation:
- The class
MyCustomPlugin
implementsFlutterPlugin
andMethodChannel.MethodCallHandler
. onAttachedToEngine
is called when the plugin is attached to the Flutter engine. Initialize theMethodChannel
here.onMethodCall
is invoked when a method is called from Dart. It receives aMethodCall
object with the method name and arguments.- The code retrieves the platform version using
android.os.Build.VERSION.RELEASE
and returns it as the result. - It is retreving data with
call.argument
in the nativeFunction.("argument") onDetachedFromEngine
is called when the plugin is detached from the Flutter engine. Release any resources here.
Step 4: Implement the iOS Platform Code (Swift/Objective-C)
Navigate to the ios/Classes/
directory within your plugin project.
Edit the auto-generated Swift file (or create an Objective-C file if you prefer) to implement the iOS-specific functionality.
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 "nativeFunction":
if let argument = call.arguments as? String {
// Native Code implementation
result("iOS Native Result: (argument)")
} else {
result(FlutterError(code: "ARGUMENT_ERROR",
message: "Argument is null",
details: nil))
}
default:
result(FlutterMethodNotImplemented)
}
}
}
Explanation:
- The class
MyCustomPlugin
conforms to theFlutterPlugin
protocol. register(with registrar:)
is the entry point for the plugin. It initializes theFlutterMethodChannel
.handle(_:result:)
is called when a method is invoked from Dart.- The code retrieves the platform version using
UIDevice.current.systemVersion
and returns it as the result. - The iOS function retreives argument with call.arguments.
Step 5: Use the Plugin in Your Flutter App
Now, you can use your custom plugin in a Flutter app. Add the plugin as a dependency in the pubspec.yaml
file of your app:
dependencies:
my_custom_plugin:
path: ../my_custom_plugin/
Run flutter pub get
to install the dependency.
Then, in your Flutter code, call the plugin methods:
import 'package:flutter/material.dart';
import 'package:my_custom_plugin/my_custom_plugin.dart';
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
String _platformVersion = 'Unknown';
String _nativeResult = 'Unknown';
@override
void initState() {
super.initState();
initPlatformState();
}
Future initPlatformState() async {
String? platformVersion;
try {
platformVersion = await MyCustomPlugin.getPlatformVersion();
} catch (e) {
platformVersion = 'Failed to get platform version.';
}
String? nativeResult;
try {
nativeResult = await MyCustomPlugin.nativeFunction("Data to Native");
} catch (e) {
nativeResult = 'Failed to get native result.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion ?? 'Unknown';
_nativeResult = nativeResult ?? 'Unknown';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Running on: $_platformVersionn'),
Text('Native Result: $_nativeResultn'),
],
),
),
);
}
}
Testing Your Plugin
Thoroughly test your plugin on both Android and iOS devices to ensure it works as expected. Use debuggers, logging, and unit tests to identify and fix issues.
Conclusion
Building custom Flutter plugins allows you to extend Flutter’s capabilities by accessing native platform features. By following the steps outlined in this guide, you can create powerful plugins that enhance your Flutter applications.