Creating Custom Flutter Plugins for Reusable Native Functionality

Flutter is a versatile UI toolkit that enables developers to build natively compiled applications for mobile, web, and desktop from a single codebase. However, there are situations where you need to access platform-specific features or native functionalities not available directly through Flutter’s framework. In such cases, creating custom Flutter plugins becomes essential. This blog post explores how to create custom Flutter plugins to reuse native functionality, providing detailed steps and examples to guide you through the process.

What are Flutter Plugins?

Flutter plugins are packages that contain platform-specific code (e.g., Swift/Objective-C for iOS, and Kotlin/Java for Android) accessible via Flutter’s platform channels. These plugins allow Flutter apps to interact with native APIs and functionalities that are not natively available in the Flutter SDK.

Why Create Custom Flutter Plugins?

  • Access Native Functionality: Implement features such as accessing device sensors, custom UI components, or specific system services.
  • Reusability: Package platform-specific functionality into reusable components.
  • Performance: Leverage native code for performance-critical tasks.
  • Integration: Integrate existing native libraries and SDKs into Flutter apps.

Steps to Create a Custom Flutter Plugin

Step 1: Set Up Your Development Environment

Ensure you have Flutter installed and configured. Verify this by running:

flutter doctor

This command checks your environment and displays any dependencies you need to install.

Step 2: Create a New Flutter Plugin Project

Use the following command to create a new Flutter plugin project:

flutter create --template=plugin my_custom_plugin

Replace my_custom_plugin with the desired name for your plugin. This command generates the basic project structure including:

  • android/: Contains the Android platform-specific code.
  • ios/: Contains the iOS platform-specific code.
  • lib/: Contains the Dart code that interfaces with the native code.
  • example/: A Flutter app that demonstrates how to use the plugin.

Step 3: Define the Plugin Interface in Dart

In the lib/my_custom_plugin.dart file, define the interface that Flutter developers will use to interact with your plugin. This typically includes method signatures and error handling.


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 nativeAdd(int a, int b) async {
    final int? sum = await _channel.invokeMethod('nativeAdd', {'a': a, 'b': b});
    return sum;
  }
}

Explanation:

  • MethodChannel: Used to communicate between Dart and native code.
  • getPlatformVersion: An example method to retrieve the platform version.
  • nativeAdd: An example method that takes two integers, and requests that the native plugin returns their sum.
  • invokeMethod: Invokes a method on the native platform with the given arguments.

Step 4: Implement Native Code (Android)

Navigate to the android/src/main/kotlin/com/example/my_custom_plugin/MyCustomPluginPlugin.kt file. Here, you’ll implement the native Android code that the Dart code will call.


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

/** MyCustomPluginPlugin */
class MyCustomPluginPlugin: FlutterPlugin, MethodCallHandler {
  private lateinit var channel : MethodChannel

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    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}")
      }
      "nativeAdd" -> {
        val a = call.argument("a") ?: 0
        val b = call.argument("b") ?: 0
        result.success(a + b)
      }
      else -> {
        result.notImplemented()
      }
    }
  }

  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }
}

Explanation:

  • FlutterPlugin: Implemented by the plugin class.
  • MethodChannel: Used to receive method calls from Flutter.
  • onAttachedToEngine: Called when the plugin is attached to the Flutter engine.
  • onMethodCall: Handles incoming method calls from Dart.
  • getPlatformVersion: Returns the Android platform version.
  • nativeAdd: Receives two integers a and b from Flutter and return their sum to flutter using result.success().
  • onDetachedFromEngine: Called when the plugin is detached from the Flutter engine.

Step 5: Implement Native Code (iOS)

Navigate to the ios/Classes/MyCustomPluginPlugin.m file (or MyCustomPluginPlugin.swift if you prefer Swift). Implement the iOS-specific code.


#import "MyCustomPluginPlugin.h"
#if __has_include()
#import 
#else
#import 
#endif

@implementation MyCustomPluginPlugin
+ (void)registerWithRegistrar:(NSObject*)registrar {
  FlutterMethodChannel* channel = [FlutterMethodChannel
      methodChannelWithName:@"my_custom_plugin"
            binaryMessenger:[registrar messenger]];
  MyCustomPluginPlugin* instance = [[MyCustomPluginPlugin alloc] init];
  [registrar addMethodCallDelegate:instance channel:channel];
}

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else if ([@"nativeAdd" isEqualToString:call.method]) {
      NSNumber *a = call.arguments[@"a"];
      NSNumber *b = call.arguments[@"b"];
      NSInteger sum = [a integerValue] + [b integerValue];
      result(@(sum));
  } else {
    result(FlutterMethodNotImplemented);
  }
}

@end

Explanation:

  • registerWithRegistrar: Registers the plugin with Flutter.
  • handleMethodCall: Handles incoming method calls from Dart.
  • getPlatformVersion: Returns the iOS platform version.
  • nativeAdd: Receives two integers a and b from Flutter and return their sum to flutter using result(NSNumber).

Step 6: Test the Plugin

Navigate to the example/ directory, which contains a simple Flutter app that demonstrates how to use your plugin. You can modify the main.dart file to call your plugin’s methods.


import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:my_custom_plugin/my_custom_plugin.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  String _platformVersion = 'Unknown';
  int _nativeSum = 0;

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  Future initPlatformState() async {
    String? platformVersion;
    int? nativeSum;
    try {
      platformVersion = await MyCustomPlugin.getPlatformVersion() ?? 'Unknown platform version';
      nativeSum = await MyCustomPlugin.nativeAdd(5, 3) ?? 0;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
      nativeSum = 0;
    }

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
      _nativeSum = nativeSum;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Running on: $_platformVersionn'),
              Text('Native sum: $_nativeSumn'),
            ],
          ),
        ),
      ),
    );
  }
}

To run the example app:

cd example
flutter run

This compiles and runs the example app on your connected device or emulator.

Step 7: Publish the Plugin (Optional)

If you want to share your plugin with the Flutter community, you can publish it to pub.dev.

  • Update pubspec.yaml: Add details such as the plugin description, authors, and repository.
  • Prepare Documentation: Write detailed documentation for your plugin, including installation instructions, API usage, and examples.
  • Publish: Run flutter pub publish to publish the plugin.

Advanced Plugin Development Techniques

Using Event Channels

For continuous data streams or asynchronous events, use EventChannel instead of MethodChannel. For example, you can use EventChannel to stream sensor data from native code to Flutter.


// Dart code
import 'package:flutter/services.dart';

class SensorPlugin {
  static const EventChannel _eventChannel =
      const EventChannel('sensor_events');

  static Stream get sensorEvents {
    return _eventChannel.receiveBroadcastStream().map((dynamic event) {
      return SensorEvent(event['x'], event['y'], event['z']);
    });
  }
}

class SensorEvent {
  final double x;
  final double y;
  final double z;

  SensorEvent(this.x, this.y, this.z);
}

Handling Complex Data

When exchanging complex data between Flutter and native code, use data serialization techniques such as JSON or Protocol Buffers to ensure data integrity and compatibility.

Conclusion

Creating custom Flutter plugins allows you to extend Flutter’s capabilities by integrating with native functionalities. By following the steps outlined in this guide, you can create reusable and efficient plugins that bridge the gap between Flutter and native platforms. Whether you’re accessing device-specific features, integrating with native libraries, or optimizing performance-critical tasks, custom plugins are a powerful tool in your Flutter development arsenal.