Building Custom Flutter Plugins

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 implements FlutterPlugin and MethodChannel.MethodCallHandler.
  • onAttachedToEngine is called when the plugin is attached to the Flutter engine. Initialize the MethodChannel here.
  • onMethodCall is invoked when a method is called from Dart. It receives a MethodCall 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("argument") in the nativeFunction.
  • 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 the FlutterPlugin protocol.
  • register(with registrar:) is the entry point for the plugin. It initializes the FlutterMethodChannel.
  • 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.