Understanding the Plugin template in Flutter

Understanding the Plugin template in Flutter

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 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).

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 and MethodCallHandler.

Properties

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.

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 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.

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 to null.

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 from NSObject and conforms to the FlutterPlugin 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 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.

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:

  1. It calls invokeMethod on the methodChannel, passing 'getPlatformVersion' as the method name.

  2. This invokes a method on the native side (iOS/Android) through the method channel.

  3. 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 🐦

Did you find this article valuable?

Support Harish Kunchala by becoming a sponsor. Any amount is appreciated!