Article

Flutter Plugin Development: A Comprehensive Guide: Part 2

November 12, 2024

By Derrik Fleming | Senior Software Engineer |

In the first part of this series, we explored how to set up a Flutter plugin project using the federated plugin architecture and discussed the core principles of plugin development. Now, in Part 2, we’ll take it a step further by building a federated Flutter plugin that accesses platform sensors, focusing specifically on implementing gyroscope functionality. The foundation we establish here will be adaptable for integrating other types of sensors available on mobile platforms.

While the functionality we’re implementing is similar to what the sensors_plus package offers, our approach will highlight how to use the federated plugin architecture to directly access gyroscope data. By the end of this tutorial, you’ll know how to create a shared platform interface and implement the required native code in Swift and Kotlin to deliver sensor events to a Flutter application.

To make this guide practical and easy to follow, I’ve set up a GitHub repository that accompanies the tutorial. For every major section, you’ll find permalinks to specific commits that correspond to the discussed changes.

To kick things off, I created a new plugin project tailored to the gyroscope functionality we’ll be working on in this part. Here’s the command used to generate the project:

very_good create flutter_plugin flutter_sensors \
  --desc "A federated plugin providing access to platform sensors" \
  --platforms android,ios \
  --org "com.spindance.tutorial"

Federated Plugin Structure Recap

A federated plugin is structured as a collection of interconnected packages, each serving a specific purpose:

  • flutter_sensors_platform_interface: This package defines the PlatformInterface, which serves as the contract for platform-specific implementations.
  • flutter_sensors_android: Implements the Android-specific functionality using Kotlin.
  • flutter_sensors_ios: Implements the iOS-specific functionality using Swift.
  • flutter_sensors_plugin: A Dart package that provides the public-facing API used by Flutter applications.

Each platform-specific package implements the methods defined in the PlatformInterface, ensuring platform-appropriate behavior while maintaining a shared interface for the Flutter app. This modular approach is a key strength of the federated plugin architecture and ensures flexibility and maintainability as the plugin evolves.

1. Creating data classes

To effectively represent data coming from native platforms, we need structured classes that encapsulate sensor data. These classes will reside in the PlatformInterface package, ensuring consistency across platforms. Let’s start by creating an enumeration, SensorType, which will help us differentiate between various types of sensor data.

Defining Sensor Types

We begin with an enumeration that defines the available sensor types. For now, we’ll focus solely on the gyroscope. However, this setup is extensible, allowing for the addition of other sensor types later.

./flutter_sensors_platform_interface/lib/src/sensor_type.dart

/// Available sensor types
enum SensorType {
  /// The gyro sensor type
  gyro;

  @override
  String toString() => switch (this) { gyro => 'gyro' };

  /// Parses a [String] to a [SensorType]
  static SensorType parse(String value) => switch (value) {
        'gyro' => SensorType.gyro,
        _ => throw FormatException("Unknown SensorType: '$value'"),
      };
}

See the full diff here.

This enumeration provides a simple way to parse and stringify sensor types. The parse method ensures that any input string is validated against known types, throwing a FormatException if the input is invalid. For now, gyro is the only supported type, but you can extend this to include additional sensor types later. You can add to the values later if you choose to support more types of sensors, but for now we’ll focus on the gyroscope, specifically the rotational values it provides. Looking at the related docs for iOS and Android, we can see that for both platforms the sensor provides three rotational values, one for each of the x, y, and z axis.

Modeling Sensor Data

Next, we need a way to represent sensor data generically while supporting specific implementations for different sensor types. For this, we’ll create an abstract base class, SensorData, and a specialized class, GyroSensorData, for gyroscope data.

./flutter_sensors_platform_interface/lib/src/sensor_data.dart

/// Generic wrapper for data from sensors
abstract class SensorData {
  /// Create a [SensorData]
  SensorData();

  /// Creates an instance of SensorData from a decoded JSON blob
  factory SensorData.fromJson(Map<String, dynamic> json) {
    final type = SensorType.parse(json['type'].toString());
    return switch (type) {
      SensorType.gyro => GyroSensorData.fromJson(json),
    };
  }
}

See the full diff here.

The SensorData class serves as the base type for all sensor data. Its fromJson factory method dynamically determines the sensor type from the JSON input and delegates to the appropriate subclass. Currently, it only supports GyroSensorData, but this design makes it easy to add support for other sensors later.

./flutter_sensors_platform_interface/lib/src/gyro_sensor_data.dart

/// Encapsulates the data obtained from a [SensorType.gyro]
class GyroSensorData extends SensorData {
  /// Creates a new instance of [GyroSensorData]
  GyroSensorData({
    required this.x,
    required this.y,
    required this.z,
  });

  /// Creates a new instance of [GyroSensorData] from a decoded JSON blob
  factory GyroSensorData.fromJson(Map<String, dynamic> json) {
    if (json['x'] is num && json['y'] is num && json['z'] is num) {
      return GyroSensorData(
        x: (json['x'] as num).toDouble(),
        y: (json['y'] as num).toDouble(),
        z: (json['z'] as num).toDouble(),
      );
    } else {
      throw const FormatException('Invalid format for Sensor Data.');
    }
  }

  /// The X-axis value for [SensorType.gyro] [SensorData]
  final double x;

  /// the Y-axis value for [SensorType.gyro] [SensorData]
  final double y;

  /// The Z-axis value for [SensorType.gyro] [SensorData]
  final double z;
}

See the full diff here.

The GyroSensorData class represents the three rotational values (x, y, and z) provided by the gyroscope. These values are parsed from JSON and converted to double to ensure type consistency. The fromJson factory method validates the input to guarantee that the expected data is present and correctly formatted. If the input does not meet these criteria, a FormatException is thrown.

Key Considerations

  1. Extensibility: Both the SensorType and SensorData classes are designed to support additional sensor types in the future. Adding a new sensor type will only require extending the enumeration, creating a corresponding data class, and updating the factory in the parent class SensorData.
  2. Manual Serialization: For simplicity, we’re handling serialization manually. This approach avoids dependencies but feel free to use a serialization library (e.g., freezed, json_serializable) if you prefer.

Now that we have a good idea of what our data will look like, how should the platform provide it to a Flutter application?

Setting up the Platform Interface

To enable communication between Flutter and the native platform, we need to establish a platform interface. In the first part of this series, we explored Flutter’s two main channel types: MethodChannel and EventChannel. For our use case, EventChannel is a natural fit because it allows us to stream continuous sensor data efficiently.

We’ll begin by setting up the EventChannel in the PlatformInterface package. Additionally, we’ll configure a default implementation using MethodChannel, which the very_good_cli tool has already scaffolded. Let’s extend this setup to include EventChannel functionality.

Adding the Event Channel

We’ll modify the implementation file to define both a MethodChannel and an EventChannel. This configuration ensures we can invoke methods on the native platform and receive real-time data streams.

./flutter_sensors_platform_interface/lib/src/channels_flutter_sensors.dart

/// An implementation of [FlutterSensorsPlatform] that uses method channels.
class ChannelsFlutterSensors extends FlutterSensorsPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('flutter_sensors');

  /// The event channel used to recieve events from the native platform
  @visibleForTesting
  final EventChannel eventChannel =
      const EventChannel('flutter_sensors/sensor_events');

  @override
  Future<String?> getPlatformName() {
    return methodChannel.invokeMethod<String>('getPlatformName');
  }

  @override
  Stream<SensorData> get sensorData {
    throw UnimplementedError();
  }
}

Here, we’ve introduced an EventChannel alongside the existing MethodChannel. While the MethodChannel is used for one-off calls like retrieving the platform name, the EventChannel will handle streaming sensor data.

Key Points:

  1. EventChannel for Streaming: The eventChannel is initialized with a unique name, flutter_sensors/sensor_events, which matches the identifier used in native code to publish sensor events.
  2. Default Implementation: The sensorData getter is defined but left unimplemented here. Each platform will provide its own implementation later, ensuring platform-specific functionality.

See the full diff here.

Note: I’ve renamed the above file from it’s original name method_channel_flutter_sensors.dart, and the class name as well to better reflect it’s contents and purpose. Also, in an effort to show implementations specific to each platform we won’t implement sensorData here, though we could as our implementations (at least in Dart) will be be identical for each platform.

Updating the Platform Interface

Next, we’ll make adjustments to the PlatformInterface itself. This step prepares the interface for platform-specific implementations and ensures that our ChannelsFlutterSensors class serves as the default instance.

./flutter_sensors_platform_interface/lib/flutter_sensors_platform_interface.dart

abstract class FlutterSensorsPlatform extends PlatformInterface {
  /// Constructs a FlutterSensorsPlatform.
  FlutterSensorsPlatform() : super(token: _token);

  static final Object _token = Object();

  static FlutterSensorsPlatform _instance = ChannelsFlutterSensors();

  /// The default instance of [FlutterSensorsPlatform] to use.
  ///
  /// Defaults to [ChannelsFlutterSensors].
  static FlutterSensorsPlatform get instance => _instance;

  /// Platform-specific plugins should set this with their own platform-specific
  /// class that extends [FlutterSensorsPlatform] when they register themselves.
  static set instance(FlutterSensorsPlatform instance) {
    PlatformInterface.verify(instance, _token);
    _instance = instance;
  }

  /// Return the current platform name.
  Future<String?> getPlatformName();

  /// Return a stream of [SensorData] from the native platform
  Stream<SensorData> get sensorData;
}

See the full diff here.

This abstract class defines the contract for all platform-specific implementations, ensuring that every implementation includes:

  • A getPlatformName() method to identify the platform.
  • A sensorData stream to provide real-time sensor readings.

The instance getter and setter handle the singleton pattern, ensuring only one implementation is active at a time. By default, this is set to ChannelsFlutterSensors, but platform-specific packages (e.g., flutter_sensors_android) will override it during registration.

Implementing the Platform Channels for iOS

With the groundwork for platform channels in place, we can now implement the iOS-specific functionality. This involves creating a concrete Dart implementation for sensorData and setting up the corresponding EventChannel for iOS. Then, we’ll move on to implementing the native iOS code using Swift.

Adding the iOS-Specific Dart Implementation

First, we define the iOS-specific implementation in the plugin’s Dart code. This class uses both MethodChannel and EventChannel to communicate with the iOS native code.

./flutter_sensors_ios/lib/flutter_sensors_ios.dart

/// The iOS implementation of [FlutterSensorsPlatform].
class FlutterSensorsIOS extends FlutterSensorsPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('flutter_sensors_ios');

  /// The event channel used to get [SensorData] events from the native platform.
  @visibleForTesting
  static const EventChannel sensorChannel =
      EventChannel('flutter_sensors_ios/sensor_events');

  /// Registers this class as the default instance of [FlutterSensorsPlatform]
  static void registerWith() {
    FlutterSensorsPlatform.instance = FlutterSensorsIOS();
  }

  @override
  Future<String?> getPlatformName() {
    return methodChannel.invokeMethod<String>('getPlatformName');
  }

  @override
  Stream<SensorData> get sensorData =>
      sensorChannel.receiveBroadcastStream().map(
            (event) => SensorData.fromJson(
              jsonDecode(event.toString()) as Map<String, dynamic>,
            ),
          );
}

See the full diff here.

  • MethodChannel: Used to handle one-time calls like retrieving the platform name (getPlatformName).
  • EventChannel: Configured to receive real-time sensor data from the native iOS platform (sensorChannel).
  • Mapping Events to SensorData: The incoming data from the EventChannel is JSON-encoded. We decode it and create new instances of the SensorData class using its named constructor.

Implementing Native Code in Swift

The native iOS implementation handles the heavy lifting of retrieving gyroscope data and passing it to Flutter via the EventChannel.

./flutter_sensors_ios/ios/Classes/FlutterSensorsPlugin.swift

import CoreMotion
import Flutter
import UIKit

public class FlutterSensorsPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
    private var eventSink: FlutterEventSink?
    private let motionManager = CMMotionManager()

    public static func register(with registrar: FlutterPluginRegistrar) {
        let methodChannel = FlutterMethodChannel(name: "flutter_sensors_ios", binaryMessenger: registrar.messenger())
        let eventChannel = FlutterEventChannel(
            name: "flutter_sensors_ios/sensor_events",
            binaryMessenger: registrar.messenger()
        )
        let instance = FlutterSensorsPlugin()
        registrar.addMethodCallDelegate(instance, channel: methodChannel)
        eventChannel.setStreamHandler(instance)
    }

    public func handle(_: FlutterMethodCall, result: @escaping FlutterResult) {
        result("iOS")
    }

    public func onListen(withArguments _: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
        self.eventSink = eventSink
        startGyroscope()
        return nil
    }

    public func onCancel(withArguments _: Any?) -> FlutterError? {
        stopGyroscope()
        eventSink = nil
        return nil
    }

    private func startGyroscope() {
        if motionManager.isGyroAvailable {
            motionManager.gyroUpdateInterval = 0.1
            motionManager.startGyroUpdates(to: .main) { [weak self] data, _ in
                if let data = data {
                    self?.sendSensorData(
                        type: "gyro",
                        xValue: data.rotationRate.x,
                        yValue: data.rotationRate.y,
                        zValue: data.rotationRate.z
                    )
                }
            }
        }
    }

    private func stopGyroscope() {
        motionManager.stopGyroUpdates()
    }

    private func sendSensorData(type: String, xValue: Double, yValue: Double, zValue: Double) {
        let sensorData: [String: Any] = ["type": type, "x": xValue, "y": yValue, "z": zValue]
        if let jsonData = try? JSONSerialization.data(withJSONObject: sensorData),
           let jsonString = String(data: jsonData, encoding: .utf8)
        {
            eventSink?(jsonString)
        }
    }
}

See the full diff here.

  • Core Motion Framework: Used to fetch gyroscope data via the CMMotionManager.
  • Stream Setup:
    • The EventChannel is configured in register(with:).
    • Data is streamed to Flutter using eventSink.
  • Handling Sensor Data:
    • The gyroscope is started when the onListen method is triggered.
    • Gyroscope updates are sent as JSON strings to Flutter via eventSink.

Key Points:

  1. Dart and Native Code Integration: The FlutterSensorsIOS Dart class interfaces with the native iOS code through the EventChannel and MethodChannel.
  2. Real-Time Data Streaming: Gyroscope data is streamed to Flutter, where it is decoded and converted into SensorData objects.
  3. Swift for Native Code: The implementation uses Swift, the default language for Flutter plugins, to interact with the Core Motion framework and handle gyroscope data.

With this setup, the iOS implementation of the plugin is complete. Next, we’ll turn our attention to the Android implementation, which follows a similar structure but uses Android-specific APIs.

Implementing the Platform Channels for Android

ust as with iOS, implementing the Android side of the Platform Interface requires us to connect Flutter to native code. Here, we’re focusing on the same getPlatformName() function we used in the iOS implementation, and on listening for SensorData events. The core differences will be in how we handle sensors and the platform-specific integration.

The first step is to implement the Android-specific FlutterSensorsAndroid class. In this class, we define an EventChannel to listen for sensor data events coming from the native Android platform.

./flutter_sensors_android/lib/flutter_sensors_android.dart

/// The Android implementation of [FlutterSensorsPlatform].
class FlutterSensorsAndroid extends FlutterSensorsPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('flutter_sensors_android');

  /// The event channel used to get [SensorData] events from the native platform.
  @visibleForTesting
  static const EventChannel sensorChannel =
      EventChannel('flutter_sensors_android/sensor_events');

  /// Registers this class as the default instance of [FlutterSensorsPlatform]
  static void registerWith() {
    FlutterSensorsPlatform.instance = FlutterSensorsAndroid();
  }

  @override
  Future<String?> getPlatformName() {
    return methodChannel.invokeMethod<String>('getPlatformName');
  }

  @override
  Stream<SensorData> get sensorData =>
      sensorChannel.receiveBroadcastStream().map((event) {
        return SensorData.fromJson(
          jsonDecode(event.toString()) as Map<String, dynamic>,
        );
      });
}

See the full diff here.

In the FlutterSensorsAndroid class, we handle platform-specific interactions using MethodChannel and EventChannel to perform the same actions as in the iOS version. The getPlatformName() method sends a request to the Android native platform and awaits a response. For the sensor data, we subscribe to the sensorChannel and map the incoming JSON events into Dart SensorData objects.

Native Android Code

For the native Android implementation, we use either Java or Kotlin to handle platform-specific logic. In this case, we’ll be working with Kotlin, which is the default for Android development in very_good_cli. The native Android code listens for sensor events, specifically from the gyroscope, and sends the sensor data to Flutter.

package com.spindance.tutorial

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.json.JSONObject

class FlutterSensorsPlugin :
    FlutterPlugin,
    MethodChannel.MethodCallHandler,
    EventChannel.StreamHandler,
    SensorEventListener {
    private lateinit var methodChannel: MethodChannel
    private lateinit var eventChannel: EventChannel
    private lateinit var sensorManager: SensorManager
    private var eventSink: EventChannel.EventSink? = null

    override fun onAttachedToEngine(
        @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding,
    ) {
        methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_sensors_android")
        methodChannel.setMethodCallHandler(this)
        eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "flutter_sensors_android/sensor_events")
        eventChannel.setStreamHandler(this)
        sensorManager = flutterPluginBinding.applicationContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    }

    override fun onMethodCall(
        @NonNull call: MethodCall,
        @NonNull result: MethodChannel.Result,
    ) {
        if (call.method == "getPlatformName") {
            result.success("Android ${android.os.Build.VERSION.RELEASE}")
        } else {
            result.notImplemented()
        }
    }

    override fun onDetachedFromEngine(
        @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding,
    ) {
        methodChannel.setMethodCallHandler(null)
    }

    override fun onListen(
        arguments: Any?,
        events: EventChannel.EventSink?,
    ) {
        eventSink = events
        startGyroscope()
    }

    override fun onCancel(arguments: Any?) {
        stopGyroscope()
        eventSink = null
    }

    private fun startGyroscope() {
        sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)?.also { gyroscope ->
            sensorManager.registerListener(this, gyroscope, SensorManager.SENSOR_DELAY_NORMAL)
        }
    }

    private fun stopGyroscope() {
        sensorManager.unregisterListener(this)
    }

    override fun onSensorChanged(event: SensorEvent) {
        if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {
            val data = mapOf("type" to "gyro", "x" to event.values[0], "y" to event.values[1], "z" to event.values[2])
            val jsonString = JSONObject(data).toString()
            eventSink?.success(jsonString)
        }
    }

    override fun onAccuracyChanged(
        sensor: Sensor?,
        accuracy: Int,
    ) {}
}

See the full diff here.

In the Android implementation, we use the SensorManager to access the device’s sensors, specifically the gyroscope in this case. We register the sensor listener to start receiving updates and, when sensor data is available, send it to Flutter via the EventChannel. The data is wrapped in a JSONObject and serialized to a JSON string before being passed to Flutter.

Key Concepts

  1. Method Channels: These are used to communicate from Flutter to the native Android code, sending requests and receiving responses. In this case, we use it to get the platform name (getPlatformName).
  2. Event Channels: Event channels are used for continuous streams of data from native code to Flutter. The gyroscope data is sent to Flutter through this channel, allowing the Flutter app to react in real-time.
  3. SensorEventListener: This listener is essential for capturing sensor data in Android. We specifically register for the gyroscope sensor and listen for updates.

By implementing the FlutterSensorsAndroid class with these native Android components, we have achieved parity with the iOS implementation. Both platforms now have their own distinct code handling platform-specific tasks while adhering to the Flutter plugin architecture. This allows the Flutter app to seamlessly interact with native sensors on both iOS and Android devices.

Example App

Now that we have implemented the plugin and its platform-specific code for both iOS and Android, it’s time to integrate this functionality into a Flutter app. Below is a simple example app that demonstrates how to use the FlutterSensorsPlugin to display real-time sensor data, such as gyroscope values.

Here, we have a basic MyApp class, which is the entry point of the application. It uses the SensorDataDisplay widget to show the sensor data. The app displays gyroscope data using a StreamBuilder, which listens to a stream of sensor events. When new data is received, it updates the UI.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: SensorDataDisplay());
  }
}

In the above code, the MyApp class is a stateless widget that returns a MaterialApp with the SensorDataDisplay widget as its home. This is where the sensor data will be displayed to the user.

Next, we define a widget that will handle displaying the gyroscope data, the GyroSensorDataWidget. This widget takes the sensor data as input and presents it in a column, showing the X, Y, and Z values.

class GyroSensorDataWidget extends StatelessWidget {
  const GyroSensorDataWidget({
    required GyroSensorData data,
    super.key,
  }) : _data = data;

  final GyroSensorData _data;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          'Sensor: Gyro Data',
          style: Theme.of(context).textTheme.headlineSmall,
        ),
        Text(
          'X: ${_data.x.toStringAsFixed(8)}',
          style: Theme.of(context).textTheme.bodyLarge,
        ),
        Text(
          'Y: ${_data.y.toStringAsFixed(8)}',
          style: Theme.of(context).textTheme.bodyLarge,
        ),
        Text(
          'Z: ${_data.z.toStringAsFixed(8)}',
          style: Theme.of(context).textTheme.bodyLarge,
        ),
      ],
    );
  }
}

The GyroSensorDataWidget widget takes the GyroSensorData and displays it in a readable format. The data is presented with precision, showing the X, Y, and Z coordinates of the gyroscope values.

The next part is the SensorDataDisplay widget, which uses a StreamBuilder to listen to sensor data and update the UI accordingly. The StreamBuilder listens for new events and builds the UI based on the type of data received. If the data is of type GyroSensorData, it will display it using the GyroSensorDataWidget. If the data type is unknown, it shows a fallback message.

class SensorDataDisplay extends StatelessWidget {
  const SensorDataDisplay({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('FlutterSensorsPlugin Example')),
      body: Center(
        child: StreamBuilder<SensorData>(
          stream: sensorData,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const CircularProgressIndicator();
            }

            if (snapshot.hasError) {
              return Text(
                'Error: ${snapshot.error}',
                style: Theme.of(context).textTheme.headlineSmall,
              );
            }

            if (!snapshot.hasData) {
              return const Text('No sensor data available');
            }

            final data = snapshot.data!;

            return switch (data) {
              final GyroSensorData d => GyroSensorDataWidget(data: d),
              _ => const Text('Unknown data type'),
            };
          },
        ),
      ),
    );
  }
}

Here, the StreamBuilder listens for sensor data and reacts to different states like waiting, error, or receiving data. When data arrives, the switch statement checks if it’s gyroscope data and presents it accordingly. If it’s not recognized, it shows a fallback message.

And that’s it, with any luck you should be able to build the example application. When it installs and launches on your device you’ll see something like the screen recording below.

Wrapping Up

Congratulations! You’ve successfully created a federated Flutter plugin with platform-specific implementations for iOS and Android. By leveraging Platform Channels, you accessed native APIs to fetch gyroscope sensor data, taking a critical step in extending Flutter’s capabilities to interact with native hardware.

In this tutorial, you built the foundation of a robust plugin capable of streaming real-time sensor data to Flutter apps. But this is just the beginning. In the next part of this series, we’ll explore packaging, testing, and deploying your plugin. Our focus will be on making it efficient, reliable, and easy for other Flutter developers to integrate into their apps.

Challenge: Before we continue, take what you’ve learned and add support for another sensor, such as the accelerometer or magnetometer. Think about what would need to be added to support this, and consider any changes that may or may not be necessary to the plugin’s API.

Stay tuned, and happy coding!