Understanding the Plugin template in Flutter

Search for a command to run...

No comments yet. Be the first to comment.
While I generally write about Flutter. This problem has been bugging me for a while and there seem to be limited resources to find a solution. So for anyone who is looking for a solution. Here you go: Type edge:flags into the address bar and open it...
In this tutorial, we’ll go through covering interceptor, which is a very crucial feature of Dio. You can find the entire source code for the project here: dio_tasker Understanding Interceptors: What is an interceptor? An interceptor in Dio is a powe...

In this part, we’ll enhance our Task Manager app by parsing JSON responses and handling different types of errors. We’ll use Dio’s built-in error handling mechanisms. You can find the source code of the entire app here: dio_tasker Parsing JSON Respon...

While building apps in Flutter is extremely fluid and wonderfully intuitive. We obviously know that we can’t build the next TikTok or Twitter by only building offline apps. Which is why we absolutely need internet capabilities in our Flutter apps. Th...

Understanding when to use setState in Flutter is crucial for managing our app’s state effectively. Here’s a detailed guide: When to use setState ? Updating the UI: Use setState when we need to update the UI in response to changes in the internal sta...

A package is a set of programs files that provides us with a specific functionality. Say like a International Time Converter, Useful Extensions Provider or may be a custom UI with some complex animations.
There are two types of packages in Flutter:
Dart packages:
These are written in pure dart and have no platform dependency so they basically can be used anywhere Flutter runs.
In fact there are even Dart packages with no dependency on Flutter Framework, these can run in any environment: web, desktop , server and so on.
Eg: intl, custom_search_bar, http
Plugin Packages:
These are a combination of Dart and Platform specific code.
These packages commonly known as plugins, contain platform-native implementation code.
They are generally used to access platform features such as native camera, native file system or a platform specific feature like Health Data from iOS or Google pedometer data from Google Fit.
Eg: image_picker, camera_x
From here on out we'll call them by their common names:
Dart/Flutter Package is Package
Plugin Package is Plugin
When we want to create a package in Flutter, we will use the Flutter create command and set the template as package.
So here's an example code
flutter create --template=package sample_package
Let's take a look at the file structure of the package:
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── lib
│ └── sample_package.dart
├── pubspec.lock
├── pubspec.yaml
├── sample_package.iml
└── test
└── sample_package_test.dart
3 directories, 9 files
As we see there are no android or iOS folders. Because as I said a package is a pure dart implementation with no native code necessary.
Note: If you want to create a dart package with no flutter dependency use: dart create --template=package package_name
Now if we want to create a Plugin with platforms as android and iOS. We'll use the following command:
flutter create --template=plugin --platforms=android,ios sample_plugin
Not only does it come with the usual lib, android, and ios folders:

But it also comes with an example folder, where we'll write the example code on how to integrate the plugin.
Also we generally interact with native code using Method Channels. How to use Method Channels: Using Platform Channels Article
Pubspec.yaml of a Plugin.Now let's take a look at the pubspec.yaml of the plugin.
name: sample_plugin
description: "A new Flutter plugin project."
version: 0.0.1
homepage:
environment:
sdk: '>=3.4.4 <4.0.0'
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
plugin:
platforms:
android:
package: com.example.sample_plugin
pluginClass: SamplePlugin
ios:
pluginClass: SamplePlugin
So as we can see the plugin classes are defined on both Android and iOS and we are going to use them for contacting via method channels.
For a detailed explanation see: How to use Platform Channels in Flutter
Method channels allows us to communicate between Flutter and Native Platforms through method invocation on the platform side. We use:
MethodChannel on Android
FlutterMethodChannel on iOS for receiving method calls and sending a result back.
So basically the platform (Host) listens on the platform channel, and receives a message. It can use it's platform APIs to make the implementation of the logic and send back a response to the Flutter App (Client).
We get a default code from Android with an example on MethodChannel to get the build version. Let's see the code:
package com.example.sample_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
/** SamplePlugin */
class SamplePlugin: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "sample_plugin")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
package com.example.sample_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
Package Declaration: The code is part of the com.example.sample_plugin package.
Imports: The code imports necessary classes from the Android and Flutter libraries to create a Flutter plugin.
/** SamplePlugin */
class SamplePlugin: FlutterPlugin, MethodCallHandler {
SamplePlugin class implements two interfaces: FlutterPlugin and MethodCallHandler.private lateinit var channel : MethodChannel
channel: This is a MethodChannel object that will handle communication between Flutter and the native Android code. It is declared as a lateinit var, meaning it will be initialized later.override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "sample_plugin")
channel.setMethodCallHandler(this)
}
onAttachedToEngine: This method is called when the plugin is attached to the Flutter engine.
flutterPluginBinding.binaryMessenger: This is used to create a MethodChannel with the name "sample_plugin".
channel.setMethodCallHandler(this): Sets the current instance (this) as the handler for method calls coming through the channel.
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
onMethodCall: This method handles incoming method calls from Flutter.
call.method: Checks the method name of the incoming call.
getPlatformVersion: If the method name is "getPlatformVersion", it returns the Android version of the device.
result.success: Sends the result back to Flutter.
result.notImplemented: If the method is not recognized, it returns a "not implemented" response.
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
onDetachedFromEngine: This method is called when the plugin is detached from the Flutter engine.
channel.setMethodCallHandler(null): Unregisters the method call handler by setting it to null.The SamplePlugin class is a Flutter plugin for Android.
It sets up a MethodChannel for communication between Flutter and native Android code.
It handles a specific method call (getPlatformVersion) to return the Android version.
It properly manages the lifecycle of the plugin by attaching and detaching from the Flutter engine.
This code essentially enables Flutter to call native Android functions and get responses back, which is useful for our platform-specific functionality.
Now let's take a look at the default code provided by iOS. So here's the default code provided in SamplePlugin.swift
import Flutter
import UIKit
public class SamplePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "sample_plugin", binaryMessenger: registrar.messenger())
let instance = SamplePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
}
import Flutter
import UIKit
import Flutter: Imports the Flutter framework for iOS.
import UIKit: Imports the UIKit framework, which provides the necessary infrastructure for constructing and managing iOS apps.
public class SamplePlugin: NSObject, FlutterPlugin {
SamplePlugin class inherits from NSObject and conforms to the FlutterPlugin protocol. This means it can act as a Flutter plugin.public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "sample_plugin", binaryMessenger: registrar.messenger())
let instance = SamplePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
register Method: This static method is called when the plugin is registered with the Flutter engine.
FlutterMethodChannel: Creates a method channel named "sample_plugin" for communication between Flutter and native iOS code.
registrar.messenger(): Provides the binary messenger for the channel.
SamplePlugin(): Creates an instance of the SamplePlugin class.
registrar.addMethodCallDelegate: Registers the instance as a method call delegate for the channel. This means that the instance will handle method calls coming through the channel.
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
handle Method: This method handles incoming method calls from Flutter.
call.method: Checks the method name of the incoming call.
getPlatformVersion: If the method name is "getPlatformVersion", it returns the iOS version of the device.
result("iOS " + UIDevice.current.systemVersion): Sends the result back to Flutter with the iOS version.
result(FlutterMethodNotImplemented): If the method is not recognized, it returns a "not implemented" response.
The SamplePlugin class is a Flutter plugin for iOS.
It sets up a FlutterMethodChannel for communication between Flutter and native iOS code.
It handles a specific method call (getPlatformVersion) to return the iOS version.
It properly registers itself with the Flutter engine and handles method calls through the handle method.
This code enables Flutter to call native iOS functions and get responses back, which is useful for our platform-specific functionality.
Now let's look at the associated dart code provided to us in lib/sample_plugin_method_channel.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'sample_plugin_platform_interface.dart';
/// An implementation of [SamplePluginPlatform] that uses method channels.
class MethodChannelSamplePlugin extends SamplePluginPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('sample_plugin');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'sample_plugin_platform_interface.dart';
These are the import statements. The code is importing necessary Flutter packages and a local file (sample_plugin_platform_interface.dart).
/// An implementation of [SamplePluginPlatform] that uses method channels.
class MethodChannelSamplePlugin extends SamplePluginPlatform {
This defines a class named MethodChannelSamplePlugin that extends SamplePluginPlatform. It's an implementation of the platform interface using method channels.
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('sample_plugin');
This creates a MethodChannel object named methodChannel. Method channels are used for communication between Flutter (Dart) and the native platform (iOS/Android). The channel is identified by the string 'sample_plugin'.
The @visibleForTesting annotation indicates that this field is intended to be visible for testing purposes.
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
This is an overridden method from the SamplePluginPlatform class. It's an asynchronous method that returns a Future<String?>.
Inside the method:
It calls invokeMethod on the methodChannel, passing 'getPlatformVersion' as the method name.
This invokes a method on the native side (iOS/Android) through the method channel.
The result (platform version) is then returned.
In summary, this code sets up a method channel to communicate with native code, specifically to retrieve the platform version.
The example/ directory contains a simple Flutter application that depends on the created sample_plugin. Let's check out the pubspec.yaml file:
name: sample_plugin_example
description: "Demonstrates how to use the sample_plugin plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: '>=3.4.4 <4.0.0'
dependencies:
flutter:
sdk: flutter
sample_plugin:
path: ../
cupertino_icons: ^1.0.6
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
As we can see it set the path of the sample plugin.
To use the plugin package, we start by importing it into our Dart libraries like, like any other plugin:
import 'package:sample_plugin/sample_plugin.dart'
Now we can see the main.dart under example/lib/ in the following folder:
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _samplePlugin = SamplePlugin();
@override
void initState() {
super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await _samplePlugin.getPlatformVersion() ?? 'Unknown platform version';
} 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;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text('Running on: $_platformVersion\n'),
),
),
);
}
}
So we want to make note of some points here:
The method calling is async as platform messages are asynchronous.
Since platform messages may fail, we are using a try/catch PlatformException that helps to inspect errors.
!(mounted) helps us in discarding the result from the platform if the widget is removed from the tree by then.
We call setState() to notify our stateful widget to show the updated platform version from the plugin.
Creating good documentation for your project is out of scope of this article. You can read up more on this from dart website: Effective Dart Documentation.
Good luck and happy coding 🐦