What is a Package?
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
Create a Package:
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
File Structure:
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
Creating a Plugin
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
File Structure of a 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.
What is a Method Channel?
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 AndroidFlutterMethodChannel
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).
Implementing the Android Plugin:
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)
}
}
Code Explanation:
Package Declaration and Imports
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.
Class Declaration
/** SamplePlugin */
class SamplePlugin: FlutterPlugin, MethodCallHandler {
- Class Declaration: The
SamplePlugin
class implements two interfaces:FlutterPlugin
andMethodCallHandler
.
Properties
private lateinit var channel : MethodChannel
channel
: This is aMethodChannel
object that will handle communication between Flutter and the native Android code. It is declared as alateinit var
, meaning it will be initialized later.
onAttachedToEngine Method
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 aMethodChannel
with the name"sample_plugin"
.channel.setMethodCallHandler(this)
: Sets the current instance (this
) as the handler for method calls coming through the channel.
onMethodCall Method
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.
onDetachedFromEngine Method
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 tonull
.
Summary
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.
Implementing the iOS Plugin:
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)
}
}
}
Code Explanation:
Imports
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.
Class Declaration
public class SamplePlugin: NSObject, FlutterPlugin {
- Class Declaration: The
SamplePlugin
class inherits fromNSObject
and conforms to theFlutterPlugin
protocol. This means it can act as a Flutter plugin.
register Method
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 theSamplePlugin
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.
handle Method
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.
Summary
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.
The Dart API:
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;
}
}
Code Explanation:
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 themethodChannel
, 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.
Example Directory:
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.
Using the 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.
Documentation for your 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 🐦